diff --git a/.gitattributes b/.gitattributes index 22b8a97e..ff4558cd 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 596a2e7c..65de11c7 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 d6aa7ece..d59365a5 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 a495682e..e4eab87a 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 9d164d5b..3dc3ea3d 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/ApiTest.java b/api/src/test/java/io/xpipe/api/test/ApiTest.java index fe7fd482..b0899607 100644 --- a/api/src/test/java/io/xpipe/api/test/ApiTest.java +++ b/api/src/test/java/io/xpipe/api/test/ApiTest.java @@ -1,6 +1,6 @@ package io.xpipe.api.test; -import io.xpipe.beacon.BeaconDaemonController; +import io.xpipe.beacon.test.BeaconDaemonController; import io.xpipe.core.util.XPipeDaemonMode; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; 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 00000000..7c185dd2 --- /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.test.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 a7245400..61bcc1ad 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,131 +1,70 @@ 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.5' + 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" - ] -} - -import org.gradle.internal.os.OperatingSystem - -if (OperatingSystem.current() == OperatingSystem.LINUX) { - jvmRunArgs.addAll("--add-opens", "java.desktop/sun.awt.X11=io.xpipe.app") -} +apply from: "$rootDir/gradle/gradle_scripts/local_junit_suite.gradle" def extensionJarDepList = project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)).toList(); - jar { finalizedBy(extensionJarDepList) } @@ -146,14 +85,19 @@ 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.app.staging', isStage // 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 +125,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/src/localTest/java/module-info.java b/app/src/localTest/java/module-info.java new file mode 100644 index 00000000..b5f43897 --- /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; +} diff --git a/app/src/localTest/java/test/Test.java b/app/src/localTest/java/test/Test.java new file mode 100644 index 00000000..2e75f5fb --- /dev/null +++ b/app/src/localTest/java/test/Test.java @@ -0,0 +1,13 @@ +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/Main.java b/app/src/main/java/io/xpipe/app/Main.java index 71de2842..978ed928 100644 --- a/app/src/main/java/io/xpipe/app/Main.java +++ b/app/src/main/java/io/xpipe/app/Main.java @@ -14,10 +14,10 @@ public class Main { // Since this is not marked as a console application, it will not print anything when you run it in a console // So sadly there can't be a help command -// if (args.length == 1 && args[0].equals("--help")) { -// System.out.println("HELP"); -// return; -// } + // if (args.length == 1 && args[0].equals("--help")) { + // System.out.println("HELP"); + // return; + // } OperationMode.init(args); } 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 674d1574..487dbd46 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,42 @@ 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 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; @@ -52,4 +82,12 @@ public class BrowserAlerts { } return names; } + + public enum FileConflictChoice { + CANCEL, + SKIP, + SKIP_ALL, + REPLACE, + REPLACE_ALL + } } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkComp.java new file mode 100644 index 00000000..97203d41 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkComp.java @@ -0,0 +1,137 @@ +package io.xpipe.app.browser; + +import atlantafx.base.theme.Styles; +import io.xpipe.app.comp.store.StoreEntryWrapper; +import io.xpipe.app.comp.store.StoreSection; +import io.xpipe.app.comp.store.StoreSectionMiniComp; +import io.xpipe.app.comp.store.StoreViewState; +import io.xpipe.app.core.AppFont; +import io.xpipe.app.fxcomps.SimpleComp; +import io.xpipe.app.fxcomps.impl.FilterComp; +import io.xpipe.app.fxcomps.impl.HorizontalComp; +import io.xpipe.app.fxcomps.util.PlatformThread; +import io.xpipe.app.util.BooleanScope; +import io.xpipe.app.util.DataStoreCategoryChoiceComp; +import io.xpipe.app.util.FixedHierarchyStore; +import io.xpipe.app.util.ThreadHelper; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.store.ShellStore; +import javafx.beans.binding.Bindings; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.css.PseudoClass; +import javafx.geometry.Point2D; +import javafx.scene.input.DragEvent; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; + +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; +import java.util.function.Predicate; + +final class BrowserBookmarkComp extends SimpleComp { + + public static final Timer DROP_TIMER = new Timer("dnd", true); + private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected"); + private final BrowserModel model; + private Point2D lastOver = new Point2D(-1, -1); + private TimerTask activeTask; + + BrowserBookmarkComp(BrowserModel model) { + this.model = model; + } + + @Override + protected Region createSimple() { + var filterText = new SimpleStringProperty(); + var open = PlatformThread.sync(model.getSelected()); + Predicate applicable = storeEntryWrapper -> { + return (storeEntryWrapper.getEntry().getStore() instanceof ShellStore + || storeEntryWrapper.getEntry().getStore() instanceof FixedHierarchyStore) + && storeEntryWrapper.getEntry().getValidity().isUsable(); + }; + var selectedCategory = new SimpleObjectProperty<>( + StoreViewState.get().getActiveCategory().getValue()); + var section = StoreSectionMiniComp.createList( + StoreSection.createTopLevel( + StoreViewState.get().getAllEntries(), storeEntryWrapper -> true, filterText, selectedCategory), + (s, comp) -> { + BooleanProperty busy = new SimpleBooleanProperty(false); + comp.disable(Bindings.createBooleanBinding( + () -> { + return busy.get() || !applicable.test(s.getWrapper()); + }, + busy)); + comp.apply(struc -> { + open.addListener((observable, oldValue, newValue) -> { + struc.get() + .pseudoClassStateChanged( + SELECTED, + newValue != null + && newValue.getEntry() + .get() + .equals(s.getWrapper() + .getEntry())); + }); + struc.get().setOnAction(event -> { + ThreadHelper.runFailableAsync(() -> { + var entry = s.getWrapper().getEntry(); + if (!entry.getValidity().isUsable()) { + return; + } + + if (entry.getStore() instanceof ShellStore fileSystem) { + model.openFileSystemAsync(entry.ref(), null, busy); + } else if (entry.getStore() instanceof FixedHierarchyStore) { + BooleanScope.execute(busy, () -> { + s.getWrapper().refreshChildren(); + }); + } + }); + event.consume(); + }); + }); + }); + var category = new DataStoreCategoryChoiceComp( + StoreViewState.get().getAllConnectionsCategory(), + StoreViewState.get().getActiveCategory(), + selectedCategory) + .styleClass(Styles.LEFT_PILL); + var filter = + new FilterComp(filterText).styleClass(Styles.RIGHT_PILL).hgrow().apply(struc -> {}); + + var top = new HorizontalComp(List.of(category.minWidth(Region.USE_PREF_SIZE), filter.hgrow())) + .styleClass("categories") + .apply(struc -> { + AppFont.medium(struc.get()); + struc.get().setFillHeight(true); + }) + .createRegion(); + var r = section.vgrow().createRegion(); + var content = new VBox(top, r); + content.setFillWidth(true); + + content.getStyleClass().add("bookmark-list"); + return content; + } + + private void handleHoverTimer(DataStore store, DragEvent event) { + if (lastOver.getX() == event.getX() && lastOver.getY() == event.getY()) { + return; + } + + lastOver = (new Point2D(event.getX(), event.getY())); + activeTask = new TimerTask() { + @Override + public void run() { + if (activeTask != this) {} + + // Platform.runLater(() -> model.openExistingFileSystemIfPresent(store.asNeeded())); + } + }; + DROP_TIMER.schedule(activeTask, 500); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserBreadcrumbBar.java b/app/src/main/java/io/xpipe/app/browser/BrowserBreadcrumbBar.java index 0851a1f0..ac4b455f 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserBreadcrumbBar.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserBreadcrumbBar.java @@ -66,7 +66,7 @@ public class BrowserBreadcrumbBar extends SimpleComp { var elements = FileNames.splitHierarchy(val); var modifiedElements = new ArrayList<>(elements); if (val.startsWith("/")) { - modifiedElements.add(0, "/"); + modifiedElements.addFirst("/"); } Breadcrumbs.BreadCrumbItem items = Breadcrumbs.buildTreeModel(modifiedElements.toArray(String[]::new)); 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 e7cd49da..d3a09b82 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; @@ -24,18 +24,6 @@ import java.util.stream.Collectors; public class BrowserClipboard { - @Value - public static class Instance { - UUID uuid; - FileSystem.FileEntry baseDirectory; - List entries; - - public String toClipboardString() { - return entries.stream().map(fileEntry -> "\"" + fileEntry.getPath() + "\"").collect( - Collectors.joining(ShellDialects.getPlatformDefault().getNewLine().getNewLineString())); - } - } - public static final Property currentCopyClipboard = new SimpleObjectProperty<>(); public static Instance currentDragClipboard; @@ -45,7 +33,7 @@ public class BrowserClipboard { .addFlavorListener(e -> ThreadHelper.runFailableAsync(new FailableRunnable<>() { @Override @SuppressWarnings("unchecked") - public void run() throws Throwable { + public void run() { Clipboard clipboard = (Clipboard) e.getSource(); try { if (!clipboard.isDataFlavorAvailable(DataFlavor.javaFileListFlavor)) { @@ -53,7 +41,8 @@ public class BrowserClipboard { } List data = (List) clipboard.getData(DataFlavor.javaFileListFlavor); - var files = data.stream().map(string -> string.toPath()).toList(); + var files = + data.stream().map(string -> string.toPath()).toList(); if (files.size() == 0) { return; } @@ -121,4 +110,20 @@ public class BrowserClipboard { return null; } + + @Value + public static class Instance { + UUID uuid; + FileSystem.FileEntry baseDirectory; + List entries; + + public String toClipboardString() { + return entries.stream() + .map(fileEntry -> "\"" + fileEntry.getPath() + "\"") + .collect(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 150394b3..94e3e130 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserComp.java @@ -58,34 +58,33 @@ public class BrowserComp extends SimpleComp { FileIconManager.loadIfNecessary(); }); - var bookmarksList = new BrowserBookmarkList(model).vgrow(); - var localDownloadStage = new BrowserTransferComp(model.getLocalTransfersStage()).hide( - PlatformThread.sync(Bindings.createBooleanBinding(() -> { - if (model.getOpenFileSystems().size() == 0) { - return true; - } + var bookmarksList = new BrowserBookmarkComp(model).vgrow(); + var localDownloadStage = new BrowserTransferComp(model.getLocalTransfersStage()) + .hide(PlatformThread.sync(Bindings.createBooleanBinding( + () -> { + if (model.getOpenFileSystems().size() == 0) { + return true; + } - if (model.getMode().isChooser()) { - return true; - } + if (model.getMode().isChooser()) { + return true; + } - // Also show on local - if (model.getSelected().getValue() != null) { - // return model.getSelected().getValue().isLocal(); - } - - return false; - }, model.getOpenFileSystems(), model.getSelected()))); + return false; + }, + model.getOpenFileSystems(), + model.getSelected()))); localDownloadStage.prefHeight(200); localDownloadStage.maxHeight(200); var vertical = new VerticalComp(List.of(bookmarksList, localDownloadStage)); - var splitPane = new SideSplitPaneComp(vertical, createTabs()).withInitialWidth( - AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth()).withOnDividerChange( - AppLayoutModel.get().getSavedState()::setBrowserConnectionsWidth).apply(struc -> { - struc.getLeft().setMinWidth(200); - struc.getLeft().setMaxWidth(500); - }); + var splitPane = new SideSplitPaneComp(vertical, createTabs()) + .withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth()) + .withOnDividerChange(AppLayoutModel.get().getSavedState()::setBrowserConnectionsWidth) + .apply(struc -> { + struc.getLeft().setMinWidth(200); + struc.getLeft().setMaxWidth(500); + }); var r = addBottomBar(splitPane.createRegion()); r.getStyleClass().add("browser"); // AppFont.small(r); @@ -104,12 +103,16 @@ public class BrowserComp extends SimpleComp { selected.setSpacing(10); model.getSelection().addListener((ListChangeListener) c -> { PlatformThread.runLaterIfNeeded(() -> { - selected.getChildren().setAll(c.getList().stream().map(s -> { - var field = new TextField(s.getRawFileEntry().getPath()); - field.setEditable(false); - field.setPrefWidth(500); - return field; - }).toList()); + selected.getChildren() + .setAll(c.getList().stream() + .map(s -> { + var field = + new TextField(s.getRawFileEntry().getPath()); + field.setEditable(false); + field.setPrefWidth(500); + return field; + }) + .toList()); }); }); var spacer = new Spacer(Orientation.HORIZONTAL); @@ -128,12 +131,16 @@ public class BrowserComp extends SimpleComp { } private Comp createTabs() { - var multi = new MultiContentComp(Map., ObservableValue>of(Comp.of(() -> createTabPane()), + var multi = new MultiContentComp(Map., ObservableValue>of( + Comp.of(() -> createTabPane()), BindingsHelper.persist(Bindings.isNotEmpty(model.getOpenFileSystems())), new BrowserWelcomeComp(model).apply(struc -> StackPane.setAlignment(struc.get(), Pos.CENTER_LEFT)), - Bindings.createBooleanBinding(() -> { - return model.getOpenFileSystems().size() == 0 && !model.getMode().isChooser(); - }, model.getOpenFileSystems()))); + Bindings.createBooleanBinding( + () -> { + return model.getOpenFileSystems().size() == 0 + && !model.getMode().isChooser(); + }, + model.getOpenFileSystems()))); return multi; } @@ -154,7 +161,8 @@ public class BrowserComp extends SimpleComp { map.put(v, t); tabs.getTabs().add(t); }); - tabs.getSelectionModel().select(model.getOpenFileSystems().indexOf(model.getSelected().getValue())); + tabs.getSelectionModel() + .select(model.getOpenFileSystems().indexOf(model.getSelected().getValue())); // Used for ignoring changes by the tabpane when new tabs are added. We want to perform the selections manually! var modifying = new SimpleBooleanProperty(); @@ -170,9 +178,9 @@ public class BrowserComp extends SimpleComp { return; } - var source = map.entrySet() - .stream() - .filter(openFileSystemModelTabEntry -> openFileSystemModelTabEntry.getValue().equals(newValue)) + var source = map.entrySet().stream() + .filter(openFileSystemModelTabEntry -> + openFileSystemModelTabEntry.getValue().equals(newValue)) .findAny() .map(Map.Entry::getKey) .orElse(null); @@ -187,9 +195,9 @@ public class BrowserComp extends SimpleComp { return; } - var toSelect = map.entrySet() - .stream() - .filter(openFileSystemModelTabEntry -> openFileSystemModelTabEntry.getKey().equals(newValue)) + var toSelect = map.entrySet().stream() + .filter(openFileSystemModelTabEntry -> + openFileSystemModelTabEntry.getKey().equals(newValue)) .findAny() .map(Map.Entry::getValue) .orElse(null); @@ -228,9 +236,9 @@ public class BrowserComp extends SimpleComp { tabs.getTabs().addListener((ListChangeListener) c -> { while (c.next()) { for (var r : c.getRemoved()) { - var source = map.entrySet() - .stream() - .filter(openFileSystemModelTabEntry -> openFileSystemModelTabEntry.getValue().equals(r)) + var source = map.entrySet().stream() + .filter(openFileSystemModelTabEntry -> + openFileSystemModelTabEntry.getValue().equals(r)) .findAny() .orElse(null); @@ -253,14 +261,22 @@ public class BrowserComp extends SimpleComp { ring.setMinSize(16, 16); ring.setPrefSize(16, 16); ring.setMaxSize(16, 16); - ring.progressProperty().bind(Bindings.createDoubleBinding(() -> model.getBusy().get() ? -1d : 0, PlatformThread.sync(model.getBusy()))); + ring.progressProperty() + .bind(Bindings.createDoubleBinding( + () -> model.getBusy().get() ? -1d : 0, PlatformThread.sync(model.getBusy()))); - var image = model.getEntry().get().getProvider().getDisplayIconFileName(model.getEntry().getStore()); + var image = model.getEntry() + .get() + .getProvider() + .getDisplayIconFileName(model.getEntry().getStore()); var logo = PrettyImageHelper.ofFixedSquare(image, 16).createRegion(); - tab.graphicProperty().bind(Bindings.createObjectBinding(() -> { - return model.getBusy().get() ? ring : logo; - }, PlatformThread.sync(model.getBusy()))); + tab.graphicProperty() + .bind(Bindings.createObjectBinding( + () -> { + return model.getBusy().get() ? ring : logo; + }, + PlatformThread.sync(model.getBusy()))); tab.setText(model.getName()); tab.setContent(new OpenFileSystemComp(model).createSimple()); @@ -281,12 +297,17 @@ public class BrowserComp extends SimpleComp { StackPane c = (StackPane) tabs.lookup("#" + id + " .tab-container"); c.getStyleClass().add("color-box"); - var color = DataStorage.get().getRootForEntry(model.getEntry().get()).getColor(); + var color = DataStorage.get() + .getRootForEntry(model.getEntry().get()) + .getColor(); if (color != null) { c.getStyleClass().add(color.getId()); } new FancyTooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(c); - c.addEventHandler(DragEvent.DRAG_ENTERED, mouseEvent -> Platform.runLater(() -> tabs.getSelectionModel().select(tab))); + c.addEventHandler( + DragEvent.DRAG_ENTERED, + mouseEvent -> Platform.runLater( + () -> tabs.getSelectionModel().select(tab))); }); } }); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserContextMenu.java b/app/src/main/java/io/xpipe/app/browser/BrowserContextMenu.java index 189ee190..dfc94472 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserContextMenu.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserContextMenu.java @@ -24,6 +24,17 @@ final class BrowserContextMenu extends ContextMenu { createMenu(); } + private static List resolveIfNeeded(BrowserAction action, List selected) { + return action.automaticallyResolveLinks() + ? selected.stream() + .map(browserEntry -> new BrowserEntry( + browserEntry.getRawFileEntry().resolved(), + browserEntry.getModel(), + browserEntry.isSynthetic())) + .toList() + : selected; + } + private void createMenu() { AppFont.normal(this.getStyleableNode()); @@ -81,7 +92,10 @@ final class BrowserContextMenu extends ContextMenu { } m.setDisable(!a.isActive(model, used)); - if (la.getProFeatureId() != null && !LicenseProvider.get().getFeature(la.getProFeatureId()).isSupported()) { + if (la.getProFeatureId() != null + && !LicenseProvider.get() + .getFeature(la.getProFeatureId()) + .isSupported()) { m.setDisable(true); m.setGraphic(new FontIcon("mdi2p-professional-hexagon")); } @@ -91,15 +105,4 @@ final class BrowserContextMenu extends ContextMenu { } } } - - private static List resolveIfNeeded(BrowserAction action, List selected) { - return action.automaticallyResolveLinks() - ? selected.stream() - .map(browserEntry -> new BrowserEntry( - browserEntry.getRawFileEntry().resolved(), - browserEntry.getModel(), - browserEntry.isSynthetic())) - .toList() - : selected; - } } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserEntry.java b/app/src/main/java/io/xpipe/app/browser/BrowserEntry.java index 8d5e50f1..bafc2e49 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserEntry.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserEntry.java @@ -2,8 +2,8 @@ package io.xpipe.app.browser; import io.xpipe.app.browser.icon.DirectoryType; import io.xpipe.app.browser.icon.FileType; -import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileKind; +import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileSystem; import lombok.Getter; diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java index fcacbe50..7a25ede4 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileListComp.java @@ -12,9 +12,9 @@ import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.HumanReadableFormat; import io.xpipe.app.util.ThreadHelper; -import io.xpipe.core.store.FileNames; import io.xpipe.core.process.OsType; import io.xpipe.core.store.FileKind; +import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileSystem; import javafx.application.Platform; import javafx.beans.binding.Bindings; @@ -81,18 +81,18 @@ final class BrowserFileListComp extends SimpleComp { filenameCol.setCellFactory(col -> new FilenameCell(fileList.getEditing())); var sizeCol = new TableColumn("Size"); - sizeCol.setCellValueFactory(param -> - new SimpleLongProperty(param.getValue().getRawFileEntry().resolved().getSize())); + sizeCol.setCellValueFactory(param -> new SimpleLongProperty( + param.getValue().getRawFileEntry().resolved().getSize())); sizeCol.setCellFactory(col -> new FileSizeCell()); var mtimeCol = new TableColumn("Modified"); - mtimeCol.setCellValueFactory(param -> - new SimpleObjectProperty<>(param.getValue().getRawFileEntry().resolved().getDate())); + mtimeCol.setCellValueFactory(param -> new SimpleObjectProperty<>( + param.getValue().getRawFileEntry().resolved().getDate())); mtimeCol.setCellFactory(col -> new FileTimeCell()); var modeCol = new TableColumn("Attributes"); - modeCol.setCellValueFactory(param -> - new SimpleObjectProperty<>(param.getValue().getRawFileEntry().resolved().getMode())); + modeCol.setCellValueFactory(param -> new SimpleObjectProperty<>( + param.getValue().getRawFileEntry().resolved().getMode())); modeCol.setCellFactory(col -> new FileModeCell()); modeCol.setSortable(false); @@ -171,7 +171,7 @@ final class BrowserFileListComp extends SimpleComp { .mapToInt(entry -> table.getItems().indexOf(entry)) .toArray(); table.getSelectionModel() - .selectIndices(table.getItems().indexOf(c.getList().get(0)), indices); + .selectIndices(table.getItems().indexOf(c.getList().getFirst()), indices); }); }); } @@ -247,12 +247,20 @@ final class BrowserFileListComp extends SimpleComp { } if (row.getItem() != null - && row.getItem().getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) { + && row.getItem() + .getRawFileEntry() + .resolved() + .getKind() + == FileKind.DIRECTORY) { return event.getButton() == MouseButton.SECONDARY; } if (row.getItem() != null - && row.getItem().getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY) { + && row.getItem() + .getRawFileEntry() + .resolved() + .getKind() + != FileKind.DIRECTORY) { return event.getButton() == MouseButton.SECONDARY || event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2; } @@ -409,9 +417,10 @@ final class BrowserFileListComp extends SimpleComp { } double proximity = 100; - Bounds tableBounds = tableView.localToScene(tableView.getBoundsInParent()); + Bounds tableBounds = tableView.localToScene(tableView.getBoundsInLocal()); double dragY = event.getSceneY(); - double topYProximity = tableBounds.getMinY() + proximity; + // Include table header as well in calculations + double topYProximity = tableBounds.getMinY() + proximity + 20; double bottomYProximity = tableBounds.getMaxY() - proximity; // clamp new values between 0 and 1 to prevent scrollbar flicking around at the edges @@ -424,15 +433,60 @@ final class BrowserFileListComp extends SimpleComp { } } + private static class FileSizeCell extends TableCell { + + @Override + protected void updateItem(Number fileSize, boolean empty) { + super.updateItem(fileSize, empty); + if (empty || getTableRow() == null || getTableRow().getItem() == null) { + setText(null); + } else { + var path = getTableRow().getItem(); + if (path.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) { + setText(""); + } else { + setText(byteCount(fileSize.longValue())); + } + } + } + } + + private static class FileModeCell extends TableCell { + + @Override + protected void updateItem(String mode, boolean empty) { + super.updateItem(mode, empty); + if (empty || getTableRow() == null || getTableRow().getItem() == null) { + setText(null); + } else { + setText(mode); + } + } + } + + private static class FileTimeCell extends TableCell { + + @Override + protected void updateItem(Instant fileTime, boolean empty) { + super.updateItem(fileTime, empty); + if (empty) { + setText(null); + } else { + setText( + fileTime != null + ? HumanReadableFormat.date( + fileTime.atZone(ZoneId.systemDefault()).toLocalDateTime()) + : ""); + } + } + } + private class FilenameCell extends TableCell { private final StringProperty img = new SimpleStringProperty(); private final StringProperty text = new SimpleStringProperty(); - private final Node imageView = new PrettySvgComp(img, 24, 24) - .createRegion(); private final StackPane textField = new LazyTextFieldComp(text).createStructure().get(); - private final HBox graphic; private final BooleanProperty updating = new SimpleBooleanProperty(); @@ -463,7 +517,8 @@ final class BrowserFileListComp extends SimpleComp { }; text.addListener(listener); - graphic = new HBox(imageView, textField); + Node imageView = new PrettySvgComp(img, 24, 24).createRegion(); + HBox graphic = new HBox(imageView, textField); graphic.setSpacing(10); graphic.setAlignment(Pos.CENTER_LEFT); HBox.setHgrow(textField, Priority.ALWAYS); @@ -520,52 +575,4 @@ final class BrowserFileListComp extends SimpleComp { } } } - - private static class FileSizeCell extends TableCell { - - @Override - protected void updateItem(Number fileSize, boolean empty) { - super.updateItem(fileSize, empty); - if (empty || getTableRow() == null || getTableRow().getItem() == null) { - setText(null); - } else { - var path = getTableRow().getItem(); - if (path.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) { - setText(""); - } else { - setText(byteCount(fileSize.longValue())); - } - } - } - } - - private static class FileModeCell extends TableCell { - - @Override - protected void updateItem(String mode, boolean empty) { - super.updateItem(mode, empty); - if (empty || getTableRow() == null || getTableRow().getItem() == null) { - setText(null); - } else { - setText(mode); - } - } - } - - private static class FileTimeCell extends TableCell { - - @Override - protected void updateItem(Instant fileTime, boolean empty) { - super.updateItem(fileTime, empty); - if (empty) { - setText(null); - } else { - setText( - fileTime != null - ? HumanReadableFormat.date( - fileTime.atZone(ZoneId.systemDefault()).toLocalDateTime()) - : ""); - } - } - } } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileListCompEntry.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileListCompEntry.java index e7a7bb44..3a04eb42 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileListCompEntry.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileListCompEntry.java @@ -27,7 +27,8 @@ public class BrowserFileListCompEntry { private Point2D lastOver = new Point2D(-1, -1); private TimerTask activeTask; - public BrowserFileListCompEntry(TableView tv, Node row, BrowserEntry item, BrowserFileListModel model) { + public BrowserFileListCompEntry( + TableView tv, Node row, BrowserEntry item, BrowserFileListModel model) { this.tv = tv; this.row = row; this.item = item; @@ -59,14 +60,18 @@ public class BrowserFileListCompEntry { var all = tv.getItems(); var index = item != null ? all.indexOf(item) : all.size() - 1; - var min = Math.min(index, tv.getSelectionModel().getSelectedIndices().stream() - .mapToInt(value -> value) - .min() - .orElse(1)); - var max = Math.max(index, tv.getSelectionModel().getSelectedIndices().stream() - .mapToInt(value -> value) - .max() - .orElse(all.indexOf(item))); + var min = Math.min( + index, + tv.getSelectionModel().getSelectedIndices().stream() + .mapToInt(value -> value) + .min() + .orElse(1)); + var max = Math.max( + index, + tv.getSelectionModel().getSelectedIndices().stream() + .mapToInt(value -> value) + .max() + .orElse(all.indexOf(item))); var toSelect = new ArrayList(); for (int i = min; i <= max; i++) { @@ -98,13 +103,15 @@ public class BrowserFileListCompEntry { return false; } - if (!Objects.equals(model.getFileSystemModel().getFileSystem(), cb.getEntries().get(0).getFileSystem())) { + if (!Objects.equals( + model.getFileSystemModel().getFileSystem(), + cb.getEntries().getFirst().getFileSystem())) { return true; } // Prevent drag and drops of files into the current directory - if (cb.getBaseDirectory() != null && cb - .getBaseDirectory() + if (cb.getBaseDirectory() != null + && cb.getBaseDirectory() .getPath() .equals(model.getFileSystemModel().getCurrentDirectory().getPath()) && (item == null || item.getRawFileEntry().getKind() != FileKind.DIRECTORY)) { diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java index 30a7bd3b..aab3adda 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileListModel.java @@ -2,8 +2,8 @@ package io.xpipe.app.browser; import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.issue.ErrorEvent; -import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileKind; +import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileSystem; import javafx.beans.property.Property; import javafx.beans.property.SimpleBooleanProperty; @@ -99,8 +99,9 @@ public final class BrowserFileListModel { path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY); var comp = comparatorProperty.getValue(); - Comparator us = - comp != null ? syntheticFirst.thenComparing(dirsFirst).thenComparing(comp) : syntheticFirst.thenComparing(dirsFirst); + Comparator us = comp != null + ? syntheticFirst.thenComparing(dirsFirst).thenComparing(comp) + : syntheticFirst.thenComparing(dirsFirst); l.sort(us); } @@ -110,14 +111,17 @@ public final class BrowserFileListModel { boolean exists; try { - exists = fileSystemModel.getFileSystem().fileExists(newFullPath) || fileSystemModel.getFileSystem().directoryExists(newFullPath); + exists = fileSystemModel.getFileSystem().fileExists(newFullPath) + || fileSystemModel.getFileSystem().directoryExists(newFullPath); } catch (Exception e) { ErrorEvent.fromThrowable(e).handle(); return false; } if (exists) { - ErrorEvent.fromMessage("Target " + newFullPath + " does already exist").expected().handle(); + ErrorEvent.fromMessage("Target " + newFullPath + " does already exist") + .expected() + .handle(); fileSystemModel.refresh(); return false; } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFilterComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserFilterComp.java index f9c731b3..f1846e14 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFilterComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFilterComp.java @@ -16,6 +16,14 @@ import org.kordamp.ikonli.javafx.FontIcon; public class BrowserFilterComp extends Comp { + private final OpenFileSystemModel model; + private final Property filterString; + + public BrowserFilterComp(OpenFileSystemModel model, Property filterString) { + this.model = model; + this.filterString = filterString; + } + @Override public Structure createBase() { var expanded = new SimpleBooleanProperty(); @@ -98,12 +106,4 @@ public class BrowserFilterComp extends Comp { return box; } } - - private final OpenFileSystemModel model; - private final Property filterString; - - public BrowserFilterComp(OpenFileSystemModel model, Property filterString) { - this.model = model; - this.filterString = filterString; - } } 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 094c43a2..009382a9 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 5754b777..82d47c21 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserModel.java @@ -33,6 +33,7 @@ public class BrowserModel { private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this); private final ObservableList selection = FXCollections.observableArrayList(); private final BrowserSavedState savedState; + @Setter private Consumer> onFinish; @@ -70,12 +71,21 @@ 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) { savedState.save(); } } + + // Delete all files + localTransfersStage.clear(); } public void finishChooser() { @@ -95,8 +105,10 @@ public class BrowserModel { return; } - var stores = chosen.stream().map( - entry -> new FileReference(selected.getValue().getEntry(), entry.getRawFileEntry().getPath())).toList(); + var stores = chosen.stream() + .map(entry -> new FileReference( + selected.getValue().getEntry(), entry.getRawFileEntry().getPath())) + .toList(); onFinish.accept(stores); } @@ -107,8 +119,11 @@ public class BrowserModel { } private void closeFileSystemSync(OpenFileSystemModel open) { - if (DataStorage.get().getStoreEntries().contains(open.getEntry().get()) && savedState != null && open.getCurrentPath().get() != null) { - savedState.add(new BrowserSavedState.Entry(open.getEntry().get().getUuid(), open.getCurrentPath().get())); + if (DataStorage.get().getStoreEntries().contains(open.getEntry().get()) + && savedState != null + && open.getCurrentPath().get() != null) { + savedState.add(new BrowserSavedState.Entry( + open.getEntry().get().getUuid(), open.getCurrentPath().get())); } open.closeSync(); synchronized (BrowserModel.this) { @@ -116,7 +131,10 @@ public class BrowserModel { } } - public void openFileSystemAsync(DataStoreEntryRef store, FailableFunction path, BooleanProperty externalBusy) { + public void openFileSystemAsync( + DataStoreEntryRef store, + FailableFunction path, + BooleanProperty externalBusy) { if (store == null) { return; } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java b/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java index 2849892c..b39e0f0f 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java @@ -106,7 +106,6 @@ public class BrowserNavBar extends SimpleComp { }) .augment(new SimpleCompStructure<>(homeButton)); - var historyButton = new Button(null, new FontIcon("mdi2h-history")); historyButton.setAccessibleText("History"); historyButton.getStyleClass().add(Styles.RIGHT_PILL); @@ -146,7 +145,6 @@ public class BrowserNavBar extends SimpleComp { .maxHeightProperty() .bind(((Region) struc.get().getChildren().get(1)).heightProperty()); - ((Region) struc.get().getChildren().get(2)) .minHeightProperty() .bind(((Region) struc.get().getChildren().get(1)).heightProperty()); @@ -197,7 +195,8 @@ public class BrowserNavBar extends SimpleComp { cm.getItems().add(current); } - var b = model.getHistory().getBackwardHistory(Integer.MAX_VALUE).stream().toList(); + var b = model.getHistory().getBackwardHistory(Integer.MAX_VALUE).stream() + .toList(); if (!b.isEmpty()) { cm.getItems().add(new SeparatorMenuItem()); } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserSavedState.java b/app/src/main/java/io/xpipe/app/browser/BrowserSavedState.java index d0b3b2e1..396a4548 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserSavedState.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserSavedState.java @@ -9,7 +9,7 @@ import java.util.UUID; public interface BrowserSavedState { - public void add(Entry entry); + void add(Entry entry); void save(); @@ -18,7 +18,7 @@ public interface BrowserSavedState { @Value @Jacksonized @Builder - public static class Entry { + class Entry { UUID uuid; String path; diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserSavedStateImpl.java b/app/src/main/java/io/xpipe/app/browser/BrowserSavedStateImpl.java index f96e9999..55d498c7 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserSavedStateImpl.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserSavedStateImpl.java @@ -20,12 +20,6 @@ import java.util.List; @JsonDeserialize(using = BrowserSavedStateImpl.Deserializer.class) public class BrowserSavedStateImpl implements BrowserSavedState { - static BrowserSavedStateImpl load() { - return AppCache.get("browser-state", BrowserSavedStateImpl.class, () -> { - return new BrowserSavedStateImpl(FXCollections.observableArrayList()); - }); - } - @JsonSerialize(as = List.class) ObservableList lastSystems; @@ -33,25 +27,10 @@ public class BrowserSavedStateImpl implements BrowserSavedState { this.lastSystems = FXCollections.observableArrayList(lastSystems); } - public static class Deserializer extends StdDeserializer { - - protected Deserializer() { - super(BrowserSavedStateImpl.class); - } - - @Override - @SneakyThrows - public BrowserSavedStateImpl deserialize(JsonParser p, DeserializationContext ctxt) { - var tree = (ObjectNode) JacksonMapper.getDefault().readTree(p); - JavaType javaType = JacksonMapper.getDefault() - .getTypeFactory() - .constructCollectionLikeType(List.class, Entry.class); - List ls = JacksonMapper.getDefault().treeToValue(tree.remove("lastSystems"), javaType); - if (ls == null) { - ls = List.of(); - } - return new BrowserSavedStateImpl(ls); - } + static BrowserSavedStateImpl load() { + return AppCache.get("browser-state", BrowserSavedStateImpl.class, () -> { + return new BrowserSavedStateImpl(FXCollections.observableArrayList()); + }); } @Override @@ -72,4 +51,24 @@ public class BrowserSavedStateImpl implements BrowserSavedState { public ObservableList getEntries() { return lastSystems; } + + public static class Deserializer extends StdDeserializer { + + protected Deserializer() { + super(BrowserSavedStateImpl.class); + } + + @Override + @SneakyThrows + public BrowserSavedStateImpl deserialize(JsonParser p, DeserializationContext ctxt) { + var tree = (ObjectNode) JacksonMapper.getDefault().readTree(p); + JavaType javaType = + JacksonMapper.getDefault().getTypeFactory().constructCollectionLikeType(List.class, Entry.class); + List ls = JacksonMapper.getDefault().treeToValue(tree.remove("lastSystems"), javaType); + if (ls == null) { + ls = List.of(); + } + return new BrowserSavedStateImpl(ls); + } + } } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java index 55f242b9..cc7265bf 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java @@ -31,6 +31,13 @@ import java.util.function.Function; @AllArgsConstructor public class BrowserSelectionListComp extends SimpleComp { + ObservableList list; + Function> nameTransformation; + + public BrowserSelectionListComp(ObservableList list) { + this(list, entry -> new SimpleStringProperty(FileNames.getFileName(entry.getPath()))); + } + public static Image snapshot(ObservableList list) { var r = new BrowserSelectionListComp(list).styleClass("drag").createRegion(); var scene = new Scene(r); @@ -41,13 +48,6 @@ public class BrowserSelectionListComp extends SimpleComp { return r.snapshot(parameters, null); } - ObservableList list; - Function> nameTransformation; - - public BrowserSelectionListComp(ObservableList list) { - this(list, entry -> new SimpleStringProperty(FileNames.getFileName(entry.getPath()))); - } - @Override protected Region createSimple() { var c = new ListBoxViewComp<>(list, list, entry -> { 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 ffc6807b..4cdbfab2 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,57 @@ 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 { + var name = (model.getProgress().getValue().getName() != null + ? " @ " + model.getProgress().getValue().getName() + " " + : ""); + return transferredCount.getValue() + " / " + allCount.getValue() + name; + } + }, + transferredCount, + allCount, + model.getProgress())); + return progressComp; + } + + private Comp createClipboardStatus() { var cc = PlatformThread.sync(BrowserClipboard.currentCopyClipboard); var ccCount = Bindings.createStringBinding( () -> { @@ -32,7 +85,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 +102,6 @@ public class BrowserStatusBarComp extends SimpleComp { .count(); }, model.getFileList().getAll())); - var selectedComp = new LabelComp(Bindings.createStringBinding( () -> { if (selectedCount.getValue().intValue() == 0) { @@ -57,19 +112,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 e9db5190..c3fe6cee 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,61 @@ 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 list = new BrowserSelectionListComp(binding, entry -> Bindings.createStringBinding(() -> { - var sourceItem = stage.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("?"); - return FileNames.getFileName(entry.getPath()) + " (" + name + ")"; - }, stage.getAllDownloaded())) + var binding = BindingsHelper.mappedContentBinding(model.getItems(), item -> item.getFileEntry()); + var list = new BrowserSelectionListComp( + binding, + entry -> Bindings.createStringBinding( + () -> { + var sourceItem = model.getItems().stream() + .filter(item -> item.getFileEntry() == entry) + .findAny(); + if (sourceItem.isEmpty()) { + return "?"; + } + var name = + sourceItem.get().downloadFinished().get() + ? "Local" + : DataStorage.get() + .getStoreDisplayName(entry.getFileSystem() + .getStore()) + .orElse("?"); + return FileNames.getFileName(entry.getPath()) + " (" + name + ")"; + }, + 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())))) + .hide(PlatformThread.sync(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 +106,56 @@ 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); + var drag = BrowserClipboard.retrieveDrag(event.getDragboard()); + if (drag == null) { + return; + } + + var files = drag.getEntries(); + 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(); @@ -131,40 +163,35 @@ public class BrowserTransferComp extends SimpleComp { return Optional.empty(); } - return Optional.of(file - .toRealPath() - .toFile()); + return Optional.of( + file.toRealPath().toFile()); } catch (IOException e) { throw new RuntimeException(e); } }) .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(); + // Don't clear, it might be more convenient to keep the contents + // 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 0204c9fe..93c13435 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java @@ -2,16 +2,23 @@ package io.xpipe.app.browser; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.util.BooleanScope; +import io.xpipe.app.util.ShellTemp; 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; @@ -21,8 +28,7 @@ import java.util.concurrent.Executors; @Value public class BrowserTransferModel { - private static final Path TEMP = - FileUtils.getTempDirectory().toPath().resolve("xpipe").resolve("download"); + private static final Path TEMP = ShellTemp.getLocalTempDataDirectory("download"); ExecutorService executor = Executors.newSingleThreadExecutor(r -> { Thread t = Executors.defaultThreadFactory().newThread(r); @@ -30,30 +36,32 @@ public class BrowserTransferModel { t.setName("file downloader"); return t; }); - - @Value - public static class Item { - String name; - FileSystem.FileEntry fileEntry; - Path localFile; - BooleanProperty finishedDownload = new SimpleBooleanProperty(); - } - BrowserModel browserModel; ObservableList items = FXCollections.observableArrayList(); BooleanProperty downloading = new SimpleBooleanProperty(); BooleanProperty allDownloaded = new SimpleBooleanProperty(); - public void clear() { - try { - FileUtils.deleteDirectory(TEMP.toFile()); + private void cleanDirectory() { + if (!Files.isDirectory(TEMP)) { + return; + } + + try (var ls = Files.list(TEMP)) { + var list = ls.toList(); + for (Path path : list) { + FileUtils.forceDelete(path.toFile()); + } } catch (IOException e) { ErrorEvent.fromThrowable(e).handle(); } + } + + public void clear() { + cleanDirectory(); 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 +69,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,18 +112,23 @@ 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; } try { try (var b = new BooleanScope(downloading).start()) { FileSystemHelper.dropFilesInto( - FileSystemHelper.getLocal(TEMP), - List.of(item.getFileEntry()), - true); + FileSystemHelper.getLocal(TEMP), List.of(item.getFileEntry()), true, false, progress -> { + item.getProgress().setValue(progress); + item.getOpenFileSystemModel().getProgress().setValue(progress); + }); } - item.finishedDownload.set(true); } catch (Throwable t) { ErrorEvent.fromThrowable(t).handle(); items.remove(item); @@ -97,4 +137,31 @@ public class BrowserTransferModel { allDownloaded.set(true); }); } + + @Value + public static class Item { + OpenFileSystemModel openFileSystemModel; + String name; + FileSystem.FileEntry fileEntry; + Path localFile; + 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); + } + } } 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 00000000..6dc36aed --- /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 { + + String name; + long transferred; + long total; + + 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); + } + + 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 cc817410..a08f0fe5 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; @@ -42,7 +42,9 @@ public class BrowserWelcomeComp extends SimpleComp { var vbox = new VBox(welcome, new Spacer(4, Orientation.VERTICAL)); vbox.setAlignment(Pos.CENTER_LEFT); - var img = PrettyImageHelper.ofSvg(new SimpleStringProperty("Hips.svg"), 50, 75).padding(new Insets(5, 0, 0, 0)).createRegion(); + var img = PrettyImageHelper.ofSvg(new SimpleStringProperty("Hips.svg"), 50, 75) + .padding(new Insets(5, 0, 0, 0)) + .createRegion(); var hbox = new HBox(img, vbox); hbox.setAlignment(Pos.CENTER_LEFT); hbox.setSpacing(15); @@ -68,33 +70,47 @@ public class BrowserWelcomeComp extends SimpleComp { }); var empty = Bindings.createBooleanBinding(() -> list.isEmpty(), list); - var header = new LabelComp(Bindings.createStringBinding(() -> { - 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); + var header = new LabelComp(Bindings.createStringBinding( + () -> { + 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(); + AppFont.setSize(header, 1); vbox.getChildren().add(header); var storeList = new VBox(); storeList.setSpacing(8); var listBox = new ListBoxViewComp<>(list, list, e -> { - var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid()); - var graphic = entry.get().getProvider().getDisplayIconFileName(entry.get().getStore()); - var view = PrettyImageHelper.ofFixedSize(graphic, 50, 40); - view.padding(new Insets(2, 8, 2, 8)); - var content = - JfxHelper.createNamedEntry(DataStorage.get().getStoreDisplayName(entry.get()), e.getPath(), graphic); - var disable = new SimpleBooleanProperty(); - return new ButtonComp(null, content, () -> { - ThreadHelper.runAsync(() -> { - model.restoreStateAsync(e, disable); - }); - }).accessibleText(DataStorage.get().getStoreDisplayName(entry.get())).disable(disable).styleClass("color-listBox").apply(struc -> struc.get().setMaxWidth(2000)).grow(true, false); - }).apply(struc -> { - VBox vBox = (VBox) struc.get().getContent(); - vBox.setSpacing(10); - }).hide(empty).createRegion(); + var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid()); + var graphic = entry.get() + .getProvider() + .getDisplayIconFileName(entry.get().getStore()); + var view = PrettyImageHelper.ofFixedSize(graphic, 50, 40); + view.padding(new Insets(2, 8, 2, 8)); + var content = JfxHelper.createNamedEntry( + DataStorage.get().getStoreDisplayName(entry.get()), e.getPath(), graphic); + var disable = new SimpleBooleanProperty(); + return new ButtonComp(null, content, () -> { + ThreadHelper.runAsync(() -> { + model.restoreStateAsync(e, disable); + }); + }) + .accessibleText(DataStorage.get().getStoreDisplayName(entry.get())) + .disable(disable) + .styleClass("color-listBox") + .apply(struc -> struc.get().setMaxWidth(2000)) + .grow(true, false); + }) + .apply(struc -> { + VBox vBox = (VBox) struc.get().getContent(); + vBox.setSpacing(10); + }) + .hide(empty) + .createRegion(); var layout = new VBox(); layout.getStyleClass().add("welcome"); @@ -107,9 +123,12 @@ public class BrowserWelcomeComp extends SimpleComp { layout.getChildren().add(Comp.separator().hide(empty).createRegion()); var tile = new TileButtonComp("restore", "restoreAllSessions", "mdmz-restore", actionEvent -> { - model.restoreState(state); - actionEvent.consume(); - }).grow(true, false).hide(empty).accessibleTextKey("restoreAllSessions"); + model.restoreState(state); + actionEvent.consume(); + }) + .grow(true, false) + .hide(empty) + .accessibleTextKey("restoreAllSessions"); layout.getChildren().add(tile.createRegion()); return layout; 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 a42e3914..b7d61831 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileSystemHelper.java +++ b/app/src/main/java/io/xpipe/app/browser/FileSystemHelper.java @@ -7,13 +7,22 @@ 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 { + private static final int DEFAULT_BUFFER_SIZE = 16384; + private static FileSystem localFileSystem; + public static String adjustPath(OpenFileSystemModel model, String path) { if (path == null) { return null; @@ -114,7 +123,8 @@ public class FileSystemHelper { } if (!model.getFileSystem().directoryExists(path)) { - throw ErrorEvent.unreportable(new IllegalArgumentException(String.format("Directory %s does not exist", path))); + throw ErrorEvent.unreportable( + new IllegalArgumentException(String.format("Directory %s does not exist", path))); } try { @@ -125,8 +135,6 @@ public class FileSystemHelper { } } - private static FileSystem localFileSystem; - public static FileSystem.FileEntry getLocal(Path file) throws Exception { if (localFileSystem == null) { localFileSystem = new LocalStore().createFileSystem(); @@ -144,25 +152,22 @@ 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, boolean checkConflicts) throws Exception { + var entries = files.stream() + .map(path -> { + try { + return getLocal(path); + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .toList(); + dropFilesInto(entry, entries, false, checkConflicts, progress); } public static void delete(List files) { - if (files.size() == 0) { + if (files.isEmpty()) { return; } @@ -176,22 +181,43 @@ 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, + boolean checkConflicts, + 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, checkConflicts); + progress.accept(BrowserTransferProgress.finished(file.getName(), file.getSize())); } else { - dropFileAcrossFileSystems(target, file); + dropFileAcrossFileSystems(target, file, progress, lastConflictChoice, files.size() > 1, checkConflicts); } } } 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, + boolean checkConflicts) + throws Exception { // Prevent dropping directory into itself if (source.getPath().equals(target.getPath())) { return; @@ -205,7 +231,12 @@ public class FileSystemHelper { } if (source.getKind() == FileKind.DIRECTORY && target.getFileSystem().directoryExists(targetFile)) { - throw ErrorEvent.unreportable(new IllegalArgumentException("Target directory " + targetFile + " does already exist")); + throw ErrorEvent.unreportable( + new IllegalArgumentException("Target directory " + targetFile + " does already exist")); + } + + if (checkConflicts && !handleChoice(lastConflictChoice, target.getFileSystem(), targetFile, multiple)) { + return; } if (explicitCopy) { @@ -215,7 +246,13 @@ 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, + boolean checkConflicts) throws Exception { if (target.getKind() != FileKind.DIRECTORY) { throw new IllegalStateException("Target " + target.getPath() + " is not a directory"); @@ -229,19 +266,27 @@ public class FileSystemHelper { return; } + AtomicLong totalSize = new AtomicLong(); if (source.getKind() == FileKind.DIRECTORY) { var directoryName = FileNames.getFileName(source.getPath()); flatFiles.put(source, directoryName); var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath())); List list = source.getFileSystem().listFilesRecursively(source.getPath()); - list.forEach(fileEntry -> { + for (FileSystem.FileEntry fileEntry : list) { flatFiles.put(fileEntry, FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath()))); - }); + if (fileEntry.getKind() == FileKind.FILE) { + // This one is up-to-date and does not need to be recalculated + totalSize.addAndGet(fileEntry.getSize()); + } + } } else { flatFiles.put(source, FileNames.getFileName(source.getPath())); + // Recalculate as it could have been changed meanwhile + totalSize.addAndGet(source.getFileSystem().getFileSize(source.getPath())); } + AtomicLong transferred = new AtomicLong(); for (var e : flatFiles.entrySet()) { var sourceFile = e.getKey(); var targetFile = FileNames.join(target.getPath(), e.getValue()); @@ -252,11 +297,127 @@ 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 (checkConflicts && !handleChoice( + lastConflictChoice, target.getFileSystem(), targetFile, multiple || flatFiles.size() > 1)) { + continue; + } + + InputStream inputStream = null; + OutputStream outputStream = null; + try { + var fileSize = sourceFile.getFileSystem().getFileSize(sourceFile.getPath()); + inputStream = sourceFile.getFileSystem().openInput(sourceFile.getPath()); + outputStream = target.getFileSystem().openOutput(targetFile, fileSize); + transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, progress); + inputStream.transferTo(OutputStream.nullOutputStream()); + } catch (Exception ex) { + // Mark progress as finished to reset any progress display + progress.accept(BrowserTransferProgress.finished(sourceFile.getName(), transferred.get())); + + if (inputStream != null) { + try { + inputStream.close(); + } catch (Exception om) { + // This is expected as the process control has to be killed + // When calling close, it will throw an exception when it has to kill + // ErrorEvent.fromThrowable(om).handle(); + } + } + if (outputStream != null) { + try { + outputStream.close(); + } catch (Exception om) { + // This is expected as the process control has to be killed + // When calling close, it will throw an exception when it has to kill + // 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 void transferFile( + FileSystem.FileEntry sourceFile, + InputStream inputStream, + OutputStream outputStream, + AtomicLong transferred, + AtomicLong total, + Consumer progress) + throws IOException { + // Initialize progress immediately prior to reading anything + progress.accept(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get())); + + var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, sourceFile.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(sourceFile.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 3d8b347d..e2388603 100644 --- a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java +++ b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java @@ -6,9 +6,12 @@ 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.process.ProcessControlProvider; +import io.xpipe.core.process.ShellControl; +import io.xpipe.core.process.ShellDialects; +import io.xpipe.core.process.ShellOpenFunction; import io.xpipe.core.store.*; import io.xpipe.core.util.FailableConsumer; import javafx.beans.binding.Bindings; @@ -27,20 +30,21 @@ import java.util.stream.Stream; public final class OpenFileSystemModel { private final DataStoreEntryRef entry; - private FileSystem fileSystem; private final Property filter = new SimpleStringProperty(); private final BrowserFileListModel fileList; private final ReadOnlyObjectWrapper currentPath = new ReadOnlyObjectWrapper<>(); private final OpenFileSystemHistory history = new OpenFileSystemHistory(); private final BooleanProperty busy = new SimpleBooleanProperty(); private final BrowserModel browserModel; - private OpenFileSystemSavedState savedState; - private OpenFileSystemCache cache; private final Property overlay = new SimpleObjectProperty<>(); private final BooleanProperty inOverview = new SimpleBooleanProperty(); private final String name; private final String tooltip; - private boolean local; + private final Property progress = + new SimpleObjectProperty<>(BrowserTransferProgress.empty()); + private FileSystem fileSystem; + private OpenFileSystemSavedState savedState; + private OpenFileSystemCache cache; private int customScriptsStartIndex; public OpenFileSystemModel(BrowserModel browserModel, DataStoreEntryRef entry) { @@ -56,6 +60,24 @@ 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() throws Exception { + if (fileSystem == null) { + return; + } + + var s = fileSystem.getShell(); + if (s.isPresent()) { + s.get().start(); + } + } + public void withShell(FailableConsumer c, boolean refresh) { ThreadHelper.runFailableAsync(() -> { if (fileSystem == null) { @@ -131,8 +153,13 @@ public final class OpenFileSystemModel { return Optional.empty(); } - // Start shell in case we exited - getFileSystem().getShell().orElseThrow().start(); + try { + // Start shell in case we exited + startIfNeeded(); + } catch (Exception ex) { + ErrorEvent.fromThrowable(ex).handle(); + return Optional.ofNullable(currentPath.get()); + } // Fix common issues with paths var adjustedPath = FileSystemHelper.adjustPath(this, path); @@ -158,26 +185,19 @@ 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, - fileSystem - .getShell() - .get() - .subShell(processControl -> adjustedPath, (sc) -> adjustedPath) - .withInitSnippet(new SimpleScriptSnippet( - fileSystem - .getShell() - .get() - .getShellDialect() - .getCdCommand(currentPath.get()), - ScriptSnippet.ExecutionType.BOTH))); + directory, + fileSystem.getShell().get().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 +247,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 +268,8 @@ public final class OpenFileSystemModel { return; } - FileSystemHelper.dropLocalFilesInto(entry, files); + startIfNeeded(); + FileSystemHelper.dropLocalFilesInto(entry, files, progress::setValue, true); refreshSync(); }); }); @@ -266,14 +288,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, true, browserTransferProgress -> { + progress.setValue(browserTransferProgress); + }); refreshSync(); }); }); @@ -294,9 +312,11 @@ 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))); + throw ErrorEvent.unreportable( + new IllegalStateException(String.format("Directory %s already exists", abs))); } fileSystem.mkdirs(abs); @@ -320,6 +340,7 @@ public final class OpenFileSystemModel { return; } + startIfNeeded(); var abs = FileNames.join(getCurrentDirectory().getPath(), linkName); fileSystem.symbolicLink(abs, targetFile); refreshSync(); @@ -370,14 +391,12 @@ public final class OpenFileSystemModel { BooleanScope.execute(busy, () -> { var fs = entry.getStore().createFileSystem(); if (fs.getShell().isPresent()) { - this.customScriptsStartIndex = fs.getShell().get().getInitCommands().size(); + this.customScriptsStartIndex = + fs.getShell().get().getInitCommands().size(); ProcessControlProvider.get().withDefaultScripts(fs.getShell().get()); } 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 +427,12 @@ 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/OpenFileSystemSavedState.java b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemSavedState.java index e724e21f..780fae2b 100644 --- a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemSavedState.java +++ b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemSavedState.java @@ -33,6 +33,86 @@ import java.util.stream.Collectors; @JsonDeserialize(using = OpenFileSystemSavedState.Deserializer.class) public class OpenFileSystemSavedState { + private static final Timer TIMEOUT_TIMER = new Timer(true); + private static final int STORED = 10; + + @Setter + private OpenFileSystemModel model; + + private String lastDirectory; + + @NonNull + private ObservableList recentDirectories; + + public OpenFileSystemSavedState(String lastDirectory, @NonNull ObservableList recentDirectories) { + this.lastDirectory = lastDirectory; + this.recentDirectories = recentDirectories; + } + + public OpenFileSystemSavedState() { + lastDirectory = null; + recentDirectories = FXCollections.observableList(new ArrayList<>(STORED)); + } + + static OpenFileSystemSavedState loadForStore(OpenFileSystemModel model) { + var state = AppCache.get("fs-state-" + model.getEntry().get().getUuid(), OpenFileSystemSavedState.class, () -> { + return new OpenFileSystemSavedState(); + }); + state.setModel(model); + return state; + } + + public void save() { + if (model == null) { + return; + } + + AppCache.update("fs-state-" + model.getEntry().get().getUuid(), this); + } + + public void cd(String dir) { + if (dir == null) { + lastDirectory = null; + return; + } + + lastDirectory = dir; + // After 10 seconds + TIMEOUT_TIMER.schedule( + new TimerTask() { + @Override + public void run() { + // Synchronize with platform thread + Platform.runLater(() -> { + if (model.isClosed()) { + return; + } + + if (Objects.equals(lastDirectory, dir)) { + updateRecent(dir); + save(); + } + }); + } + }, + 10000); + } + + private void updateRecent(String dir) { + var without = FileNames.removeTrailingSlash(dir); + var with = FileNames.toDirectory(dir); + recentDirectories.removeIf(recentEntry -> + Objects.equals(recentEntry.directory, without) || Objects.equals(recentEntry.directory, with)); + + var o = new RecentEntry(with, Instant.now()); + if (recentDirectories.size() < STORED) { + recentDirectories.addFirst(o); + } else { + recentDirectories.removeLast(); + recentDirectories.addFirst(o); + } + } + public static class Serializer extends StdSerializer { protected Serializer() { @@ -79,14 +159,6 @@ public class OpenFileSystemSavedState { } } - static OpenFileSystemSavedState loadForStore(OpenFileSystemModel model) { - var state = AppCache.get("fs-state-" + model.getEntry().get().getUuid(), OpenFileSystemSavedState.class, () -> { - return new OpenFileSystemSavedState(); - }); - state.setModel(model); - return state; - } - @Value @Jacksonized @Builder @@ -95,76 +167,4 @@ public class OpenFileSystemSavedState { String directory; Instant time; } - - @Setter - private OpenFileSystemModel model; - - private String lastDirectory; - - @NonNull - private ObservableList recentDirectories; - - public OpenFileSystemSavedState(String lastDirectory, @NonNull ObservableList recentDirectories) { - this.lastDirectory = lastDirectory; - this.recentDirectories = recentDirectories; - } - - private static final Timer TIMEOUT_TIMER = new Timer(true); - private static final int STORED = 10; - - public OpenFileSystemSavedState() { - lastDirectory = null; - recentDirectories = FXCollections.observableList(new ArrayList<>(STORED)); - } - - public void save() { - if (model == null) { - return; - } - - AppCache.update("fs-state-" + model.getEntry().get().getUuid(), this); - } - - public void cd(String dir) { - if (dir == null) { - lastDirectory = null; - return; - } - - lastDirectory = dir; - // After 10 seconds - TIMEOUT_TIMER.schedule( - new TimerTask() { - @Override - public void run() { - // Synchronize with platform thread - Platform.runLater(() -> { - if (model.isClosed()) { - return; - } - - if (Objects.equals(lastDirectory, dir)) { - updateRecent(dir); - save(); - } - }); - } - }, - 10000); - } - - private void updateRecent(String dir) { - var without = FileNames.removeTrailingSlash(dir); - var with = FileNames.toDirectory(dir); - recentDirectories.removeIf(recentEntry -> - Objects.equals(recentEntry.directory, without) || Objects.equals(recentEntry.directory, with)); - - var o = new RecentEntry(with, Instant.now()); - if (recentDirectories.size() < STORED) { - recentDirectories.add(0, o); - } else { - recentDirectories.remove(recentDirectories.size() - 1); - recentDirectories.add(0, o); - } - } } diff --git a/app/src/main/java/io/xpipe/app/browser/StandaloneFileBrowser.java b/app/src/main/java/io/xpipe/app/browser/StandaloneFileBrowser.java index 12c1aabb..35e70e95 100644 --- a/app/src/main/java/io/xpipe/app/browser/StandaloneFileBrowser.java +++ b/app/src/main/java/io/xpipe/app/browser/StandaloneFileBrowser.java @@ -39,7 +39,8 @@ public class StandaloneFileBrowser { }); } - public static void openSingleFile(Supplier> store, Consumer file) { + public static void openSingleFile( + Supplier> store, Consumer file) { PlatformThread.runLaterIfNeeded(() -> { var model = new BrowserModel(BrowserModel.Mode.SINGLE_FILE_CHOOSER, null); var comp = new BrowserComp(model) @@ -47,7 +48,7 @@ public class StandaloneFileBrowser { .apply(struc -> AppFont.normal(struc.get())); var window = AppWindowHelper.sideWindow(AppI18n.get("openFileTitle"), stage -> comp, false, null); model.setOnFinish(fileStores -> { - file.accept(fileStores.size() > 0 ? fileStores.get(0) : null); + file.accept(fileStores.size() > 0 ? fileStores.getFirst() : null); window.close(); }); window.show(); @@ -63,7 +64,7 @@ public class StandaloneFileBrowser { .apply(struc -> AppFont.normal(struc.get())); var window = AppWindowHelper.sideWindow(AppI18n.get("saveFileTitle"), stage -> comp, true, null); model.setOnFinish(fileStores -> { - file.setValue(fileStores.size() > 0 ? fileStores.get(0) : null); + file.setValue(fileStores.size() > 0 ? fileStores.getFirst() : null); window.close(); }); window.show(); diff --git a/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java b/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java index 36b107ca..0d2cce15 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java +++ b/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java @@ -13,14 +13,6 @@ import java.util.ServiceLoader; public interface BrowserAction { - enum Category { - CUSTOM, - OPEN, - NATIVE, - COPY_PASTE, - MUTATION - } - List ALL = new ArrayList<>(); static List getFlattened(OpenFileSystemModel model, List entries) { @@ -39,7 +31,7 @@ public interface BrowserAction { .orElseThrow(); } - default void init(OpenFileSystemModel model) throws Exception {} + default void init(OpenFileSystemModel model) {} default String getProFeatureId() { return null; @@ -75,6 +67,14 @@ public interface BrowserAction { return true; } + enum Category { + CUSTOM, + OPEN, + NATIVE, + COPY_PASTE, + MUTATION + } + class Loader implements ModuleLayerLoader { @Override diff --git a/app/src/main/java/io/xpipe/app/browser/action/BrowserActionFormatter.java b/app/src/main/java/io/xpipe/app/browser/action/BrowserActionFormatter.java index 7a8a212a..bb8111ca 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/BrowserActionFormatter.java +++ b/app/src/main/java/io/xpipe/app/browser/action/BrowserActionFormatter.java @@ -7,7 +7,7 @@ import java.util.List; public class BrowserActionFormatter { public static String filesArgument(List entries) { - return entries.size() == 1 ? entries.get(0).getOptionallyQuotedFileName() : "(" + entries.size() + ")"; + return entries.size() == 1 ? entries.getFirst().getOptionallyQuotedFileName() : "(" + entries.size() + ")"; } public static String centerEllipsis(String input, int length) { diff --git a/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java b/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java index 4e5a7779..89b7a671 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java +++ b/app/src/main/java/io/xpipe/app/browser/action/LeafAction.java @@ -52,7 +52,8 @@ public interface LeafAction extends BrowserAction { b.setDisable(!isActive(model, selected)); }); - if (getProFeatureId() != null && !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) { + if (getProFeatureId() != null + && !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) { b.setDisable(true); b.setGraphic(new FontIcon("mdi2p-professional-hexagon")); } @@ -83,7 +84,8 @@ public interface LeafAction extends BrowserAction { mi.setMnemonicParsing(false); mi.setDisable(!isActive(model, selected)); - if (getProFeatureId() != null && !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) { + if (getProFeatureId() != null + && !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) { mi.setDisable(true); mi.setText(mi.getText() + " (Pro)"); } 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 7e6d4078..39b7fe97 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,25 +24,30 @@ 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); } - @Override - public boolean isApplicable(OpenFileSystemModel model, List entries) { - return AppPrefs.get().terminalType().getValue() != null; - } - @Override public String getName(OpenFileSystemModel model, List entries) { var t = AppPrefs.get().terminalType().getValue(); return "in " + (t != null ? t.toTranslatedString() : "?"); } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return AppPrefs.get().terminalType().getValue() != null; + } }, new LeafAction() { @@ -51,7 +56,8 @@ public abstract class MultiExecuteAction implements BranchAction { model.withShell( pc -> { for (BrowserEntry entry : entries) { - var cmd = ApplicationHelper.createDetachCommand(pc, createCommand(pc, model, entry)); + var cmd = ApplicationHelper.createDetachCommand( + pc, createCommand(pc, model, entry)); pc.command(cmd) .withWorkingDirectory(model.getCurrentDirectory() .getPath()) diff --git a/app/src/main/java/io/xpipe/app/browser/icon/DirectoryType.java b/app/src/main/java/io/xpipe/app/browser/icon/DirectoryType.java index d2432504..0204e2cd 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/DirectoryType.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/DirectoryType.java @@ -9,7 +9,10 @@ import java.io.BufferedReader; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; import java.util.stream.Collectors; public interface DirectoryType { @@ -71,6 +74,12 @@ public interface DirectoryType { }); } + String getId(); + + boolean matches(FileSystem.FileEntry entry); + + String getIcon(FileSystem.FileEntry entry, boolean open); + class Simple implements DirectoryType { @Getter @@ -101,10 +110,4 @@ public interface DirectoryType { return open ? this.open.getIcon() : this.closed.getIcon(); } } - - String getId(); - - boolean matches(FileSystem.FileEntry entry); - - String getIcon(FileSystem.FileEntry entry, boolean open); } diff --git a/app/src/main/java/io/xpipe/app/browser/icon/FileType.java b/app/src/main/java/io/xpipe/app/browser/icon/FileType.java index 4b81099a..f6017aac 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/FileType.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/FileType.java @@ -53,6 +53,12 @@ public interface FileType { }); } + String getId(); + + boolean matches(FileSystem.FileEntry entry); + + String getIcon(); + @Getter class Simple implements FileType { @@ -72,7 +78,9 @@ public interface FileType { return false; } - return (entry.getExtension() != null && endings.contains("." + entry.getExtension().toLowerCase(Locale.ROOT))) || endings.contains(entry.getName()); + return (entry.getExtension() != null + && endings.contains("." + entry.getExtension().toLowerCase(Locale.ROOT))) + || endings.contains(entry.getName()); } @Override @@ -80,10 +88,4 @@ public interface FileType { return icon.getIcon(); } } - - String getId(); - - boolean matches(FileSystem.FileEntry entry); - - String getIcon(); } diff --git a/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java b/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java index 4f300789..53503c7d 100644 --- a/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java +++ b/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java @@ -9,6 +9,7 @@ import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.storage.DataStorage; import javafx.beans.binding.Bindings; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Pane; @@ -31,13 +32,14 @@ public class AppLayoutComp extends Comp> { model.getSelected()))))); var pane = new BorderPane(); - var sidebar = new SideMenuBarComp(model.getSelected(), model.getEntries()); + var sidebar = new SideMenuBarComp(model.getSelectedInternal(), model.getEntries()); pane.setCenter(multi.createRegion()); pane.setRight(sidebar.createRegion()); pane.getStyleClass().add("background"); model.getSelected().addListener((c, o, n) -> { if (o != null && o.equals(model.getEntries().get(2))) { AppPrefs.get().save(); + DataStorage.get().saveAsync(); } }); AppFont.normal(pane); 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 00000000..f206c8f6 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/base/DialogComp.java @@ -0,0 +1,103 @@ +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.List; +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); + + buttons.getChildren() + .addAll(customButtons().stream() + .map(buttonComp -> buttonComp.createRegion()) + .toList()); + 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; + } + + protected List> customButtons() { + return List.of(); + } + + @Override + public CompStructure createBase() { + var sp = scrollPane(content()).createRegion(); + 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(); + + protected Comp scrollPane(Comp content) { + var entry = content.styleClass("dialog-content"); + return Comp.of(() -> { + var entryR = entry.createRegion(); + var sp = new ScrollPane(entryR); + sp.setFitToWidth(true); + entryR.minHeightProperty().bind(sp.heightProperty()); + return sp; + }); + } + + public Comp bottom() { + return null; + } +} diff --git a/app/src/main/java/io/xpipe/app/comp/base/IntegratedTextAreaComp.java b/app/src/main/java/io/xpipe/app/comp/base/IntegratedTextAreaComp.java index 634f4847..7ed0e065 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/IntegratedTextAreaComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/IntegratedTextAreaComp.java @@ -45,7 +45,7 @@ public class IntegratedTextAreaComp extends SimpleComp { c.getChildren().addAll(textArea, pane); return c; }), - paths -> value.setValue(Files.readString(paths.get(0)))); + paths -> value.setValue(Files.readString(paths.getFirst()))); return fileDrop.createRegion(); } 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 55a97049..6cd7c417 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)) { @@ -69,8 +68,7 @@ public class LazyTextFieldComp extends Comp { SimpleChangeListener.apply(currentValue, n -> { PlatformThread.runLaterIfNeeded(() -> { // Check if control value is the same. Then don't set it as that might cause bugs - if (Objects.equals(r.getText(), n) - || (n == null && r.getText().isEmpty())) { + if (Objects.equals(r.getText(), n) || (n == null && r.getText().isEmpty())) { return; } diff --git a/app/src/main/java/io/xpipe/app/comp/base/ListBoxViewComp.java b/app/src/main/java/io/xpipe/app/comp/base/ListBoxViewComp.java index abdd3136..13e5f7f2 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ListBoxViewComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ListBoxViewComp.java @@ -66,7 +66,8 @@ public class ListBoxViewComp extends Comp> { return new SimpleCompStructure<>(scroll); } - private void refresh(VBox listView, List shown, List all, Map cache, boolean asynchronous) { + private void refresh( + VBox listView, List shown, List all, Map cache, boolean asynchronous) { Runnable update = () -> { // Clear cache of unused values cache.keySet().removeIf(t -> !all.contains(t)); diff --git a/app/src/main/java/io/xpipe/app/comp/base/ListSelectorComp.java b/app/src/main/java/io/xpipe/app/comp/base/ListSelectorComp.java index 552c53ad..c605fffc 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ListSelectorComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ListSelectorComp.java @@ -65,7 +65,8 @@ public class ListSelectorComp extends SimpleComp { if (showAllSelector) { var allSelector = new CheckBox(null); - allSelector.setSelected(values.stream().filter(t -> !disable.test(t)).count() == selected.size()); + allSelector.setSelected( + values.stream().filter(t -> !disable.test(t)).count() == selected.size()); allSelector.selectedProperty().addListener((observable, oldValue, newValue) -> { cbs.forEach(checkBox -> { if (checkBox.isDisabled()) { 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 47aa0c77..dd38eeb9 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,10 +16,8 @@ import javafx.scene.layout.StackPane; public class LoadingOverlayComp extends Comp> { - public static LoadingOverlayComp noProgress(Comp comp, ObservableValue loading) { - return new LoadingOverlayComp(comp, loading, new SimpleDoubleProperty(-1)); - } - + private static final double FPS = 30.0; + private static final double cycleDurationSeconds = 4.0; private final Comp comp; private final ObservableValue showLoading; private final ObservableValue progress; @@ -30,6 +28,10 @@ public class LoadingOverlayComp extends Comp> { this.progress = PlatformThread.sync(progress); } + public static LoadingOverlayComp noProgress(Comp comp, ObservableValue loading) { + return new LoadingOverlayComp(comp, loading, new SimpleDoubleProperty(-1)); + } + @Override public CompStructure createBase() { var compStruc = comp.createStructure(); @@ -39,6 +41,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/MarkdownComp.java b/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java index 2ae55e47..dacbcc95 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java @@ -46,11 +46,14 @@ public class MarkdownComp extends Comp> { @SneakyThrows private WebView createWebView() { var wv = new WebView(); - wv.getEngine().setUserDataDirectory(AppProperties.get().getDataDir().resolve("webview").toFile()); + wv.getEngine() + .setUserDataDirectory( + AppProperties.get().getDataDir().resolve("webview").toFile()); wv.setPageFill(Color.TRANSPARENT); - var theme = AppPrefs.get() != null && AppPrefs.get().theme.getValue().isDark() ? "web/github-markdown-dark.css" : "web/github-markdown-light.css"; - var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, theme) - .orElseThrow(); + var theme = AppPrefs.get() != null && AppPrefs.get().theme.getValue().isDark() + ? "web/github-markdown-dark.css" + : "web/github-markdown-light.css"; + var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, theme).orElseThrow(); wv.getEngine().setUserStyleSheetLocation(url.toString()); SimpleChangeListener.apply(PlatformThread.sync(markdown), val -> { diff --git a/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java b/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java index 1c28f9bb..1bc3f4c4 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ModalOverlayComp.java @@ -7,15 +7,12 @@ import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.Shortcuts; import javafx.application.Platform; import javafx.beans.property.Property; import javafx.geometry.Insets; import javafx.scene.control.Button; import javafx.scene.control.ButtonBar; import javafx.scene.control.Label; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyCodeCombination; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; @@ -23,23 +20,14 @@ import lombok.Value; public class ModalOverlayComp extends SimpleComp { + private final Comp background; + private final Property overlayContent; + public ModalOverlayComp(Comp background, Property overlayContent) { this.background = background; this.overlayContent = overlayContent; } - @Value - public static class OverlayContent { - - String titleKey; - Comp content; - String finishKey; - Runnable onFinish; - } - - private final Comp background; - private final Property overlayContent; - @Override protected Region createSimple() { var bgRegion = background.createRegion(); @@ -62,7 +50,7 @@ public class ModalOverlayComp extends SimpleComp { if (newValue.finishKey != null) { var finishButton = new Button(AppI18n.get(newValue.finishKey)); - Shortcuts.addShortcut(finishButton, new KeyCodeCombination(KeyCode.ENTER)); + finishButton.setDefaultButton(true); Styles.toggleStyleClass(finishButton, Styles.FLAT); finishButton.setOnAction(event -> { newValue.onFinish.run(); @@ -96,4 +84,13 @@ public class ModalOverlayComp extends SimpleComp { }); return pane; } + + @Value + public static class OverlayContent { + + String titleKey; + Comp content; + String finishKey; + Runnable onFinish; + } } 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 3c3f8ffb..4293638d 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 @@ -20,6 +20,9 @@ import java.util.Map; 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 final StoreEntryWrapper wrapper; private final ObservableValue state; @@ -47,7 +50,8 @@ public class OsLogoComp extends SimpleComp { return getImage(ons.getOsName()); }, - wrapper.getPersistentState(), state)); + wrapper.getPersistentState(), + state)); var hide = BindingsHelper.map(img, s -> s != null); return new StackComp(List.of( new SystemStateComp(state).hide(hide), @@ -55,9 +59,6 @@ public class OsLogoComp extends SimpleComp { .createRegion(); } - private static final Map ICONS = new HashMap<>(); - private static final String LINUX_DEFAULT = "linux-24.png"; - private String getImage(String name) { if (name == null) { return null; @@ -66,15 +67,21 @@ 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)) - .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); - }); + 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); + }); } }); } - return ICONS.entrySet().stream().filter(e->name.toLowerCase().contains(e.getKey())).findAny().map(e->e.getValue()).orElse("os/" + LINUX_DEFAULT); + return ICONS.entrySet().stream() + .filter(e -> name.toLowerCase().contains(e.getKey())) + .findAny() + .map(e -> e.getValue()) + .orElse("os/" + LINUX_DEFAULT); } } 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 b0a30f5d..83139824 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,32 @@ 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,22 +77,59 @@ 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", - () -> { + var b = new IconButtonComp("mdal-bug_report", () -> { var event = ErrorEvent.fromMessage("User Report"); if (AppLogs.get().isWriteToFile()) { event.attachment(AppLogs.get().getSessionLogsDirectory()); } 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 +138,20 @@ 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); }); @@ -102,7 +160,8 @@ public class SideMenuBarComp extends Comp> { { var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableAlert.showIfNeeded()) - .apply(new FancyTooltipAugment<>("updateAvailableTooltip")).accessibleTextKey("updateAvailableTooltip"); + .apply(new FancyTooltipAugment<>("updateAvailableTooltip")) + .accessibleTextKey("updateAvailableTooltip"); b.apply(struc -> { AppFont.setSize(struc.get(), 2); }); @@ -123,7 +182,7 @@ public class SideMenuBarComp extends Comp> { filler.setMaxHeight(3000); vbox.getChildren().add(filler); VBox.setVgrow(filler, Priority.ALWAYS); - filler.prefWidthProperty().bind(((Region) vbox.getChildren().get(0)).widthProperty()); + filler.prefWidthProperty().bind(((Region) vbox.getChildren().getFirst()).widthProperty()); vbox.getStyleClass().add("sidebar-comp"); return new SimpleCompStructure<>(vbox); diff --git a/app/src/main/java/io/xpipe/app/comp/base/SideSplitPaneComp.java b/app/src/main/java/io/xpipe/app/comp/base/SideSplitPaneComp.java index 5bfb9400..66fa7ffa 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/SideSplitPaneComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/SideSplitPaneComp.java @@ -15,6 +15,7 @@ public class SideSplitPaneComp extends Comp { private final Comp center; private Double initialWidth; private Consumer onDividerChange; + public SideSplitPaneComp(Comp left, Comp center) { this.left = left; this.center = center; @@ -36,13 +37,13 @@ public class SideSplitPaneComp extends Comp { } if (!setInitial.get() && initialWidth != null) { - r.getDividers().get(0).setPosition(initialWidth / newValue.doubleValue()); + r.getDividers().getFirst().setPosition(initialWidth / newValue.doubleValue()); setInitial.set(true); } }); SplitPane.setResizableWithParent(sidebar, false); - r.getDividers().get(0).positionProperty().addListener((observable, oldValue, newValue) -> { + r.getDividers().getFirst().positionProperty().addListener((observable, oldValue, newValue) -> { if (r.getWidth() <= 0) { return; } @@ -52,7 +53,7 @@ public class SideSplitPaneComp extends Comp { } }); r.getStyleClass().add("side-split-pane-comp"); - return new Structure(sidebar, c, r, r.getDividers().get(0)); + return new Structure(sidebar, c, r, r.getDividers().getFirst()); } public SideSplitPaneComp withInitialWidth(double val) { 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 b29d2fa5..1d2e52ea 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/SystemStateComp.java b/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java index 87289a72..d1468808 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java @@ -18,29 +18,12 @@ import org.kordamp.ikonli.javafx.StackedFontIcon; @Getter public class SystemStateComp extends SimpleComp { + private final ObservableValue state; public SystemStateComp(ObservableValue state) { this.state = state; } - public enum State { - FAILURE, - SUCCESS, - OTHER; - - public static ObservableValue shellState(StoreEntryWrapper w) { - return BindingsHelper.map(w.getPersistentState(),o -> { - if (o instanceof ShellStoreState shellStoreState) { - return shellStoreState.getRunning() != null ? shellStoreState.getRunning() ? SUCCESS : FAILURE : OTHER; - } - - return OTHER; - }); - } - } - - private final ObservableValue state; - @Override protected Region createSimple() { var icon = PlatformThread.sync(Bindings.createStringBinding( @@ -58,15 +41,19 @@ public class SystemStateComp extends SimpleComp { border.getStyleClass().add("outer-icon"); border.setOpacity(0.5); - var success = Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-success-emphasis; }"); - var failure = Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-danger-emphasis; }"); - var other = Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-accent-emphasis; }"); + var success = Styles.toDataURI( + ".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-success-emphasis; }"); + var failure = + Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-danger-emphasis; }"); + var other = + Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-accent-emphasis; }"); var pane = new StackedFontIcon(); pane.getChildren().addAll(fi, border); pane.setAlignment(Pos.CENTER); - var dataClass1 = """ + var dataClass1 = + """ .stacked-ikonli-font-icon > .outer-icon { -fx-icon-size: 22px; } @@ -78,9 +65,31 @@ public class SystemStateComp extends SimpleComp { SimpleChangeListener.apply(PlatformThread.sync(state), val -> { pane.getStylesheets().removeAll(success, failure, other); - pane.getStylesheets().add(val == State.SUCCESS ? success : val == State.FAILURE ? failure: other); + pane.getStylesheets().add(val == State.SUCCESS ? success : val == State.FAILURE ? failure : other); }); return pane; } + + public enum State { + FAILURE, + SUCCESS, + OTHER; + + public static ObservableValue shellState(StoreEntryWrapper w) { + return BindingsHelper.map(w.getPersistentState(), o -> { + if (o instanceof ShellStoreState s) { + if (s.getShellDialect() != null && !s.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) { + return SUCCESS; + } + + return s.getRunning() != null + ? s.getRunning() ? SUCCESS : FAILURE + : OTHER; + } + + return OTHER; + }); + } + } } diff --git a/app/src/main/java/io/xpipe/app/comp/base/TileButtonComp.java b/app/src/main/java/io/xpipe/app/comp/base/TileButtonComp.java index 7d30e26a..b688b567 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/TileButtonComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/TileButtonComp.java @@ -28,6 +28,18 @@ import java.util.function.Consumer; @Getter public class TileButtonComp extends Comp { + private final ObservableValue name; + private final ObservableValue description; + private final ObservableValue icon; + private final Consumer action; + + public TileButtonComp(String nameKey, String descriptionKey, String icon, Consumer action) { + this.name = AppI18n.observable(nameKey); + this.description = AppI18n.observable(descriptionKey); + this.icon = new SimpleStringProperty(icon); + this.action = action; + } + @Override public Structure createBase() { var bt = new Button(); @@ -68,7 +80,13 @@ public class TileButtonComp extends Comp { fi.setIconSize((int) (size * 0.55)); }); bt.setGraphic(hbox); - return Structure.builder().graphic(fi).button(bt).content(hbox).name(header).description(desc).build(); + return Structure.builder() + .graphic(fi) + .button(bt) + .content(hbox) + .name(header) + .description(desc) + .build(); } @Value @@ -85,16 +103,4 @@ public class TileButtonComp extends Comp { return button; } } - - private final ObservableValue name; - private final ObservableValue description; - private final ObservableValue icon; - private final Consumer action; - - public TileButtonComp(String nameKey, String descriptionKey, String icon, Consumer action) { - this.name = AppI18n.observable(nameKey); - this.description = AppI18n.observable(descriptionKey); - this.icon = new SimpleStringProperty(icon); - this.action = action; - } } diff --git a/app/src/main/java/io/xpipe/app/comp/base/ToggleSwitchComp.java b/app/src/main/java/io/xpipe/app/comp/base/ToggleSwitchComp.java new file mode 100644 index 00000000..07b9c1ba --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/base/ToggleSwitchComp.java @@ -0,0 +1,37 @@ +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.Property; +import javafx.beans.value.ObservableValue; +import javafx.scene.layout.Region; + +public class ToggleSwitchComp extends SimpleComp { + + private final Property selected; + private final ObservableValue name; + + public ToggleSwitchComp(Property selected, ObservableValue name) { + this.selected = selected; + this.name = name; + } + + @Override + protected Region createSimple() { + var s = new ToggleSwitch(); + s.setSelected(selected.getValue()); + s.selectedProperty().addListener((observable, oldValue, newValue) -> { + selected.setValue(newValue); + }); + selected.addListener((observable, oldValue, newValue) -> { + PlatformThread.runLaterIfNeeded(() -> { + s.setSelected(newValue); + }); + }); + if (name != null) { + s.textProperty().bind(PlatformThread.sync(name)); + } + return s; + } +} 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 8635ee8e..61271b61 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,26 @@ 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; } @@ -51,9 +60,12 @@ public class DenseStoreEntryComp extends StoreEntryComp { grid.setHgap(8); var name = createName().createRegion(); - name.maxWidthProperty().bind(Bindings.createDoubleBinding(() -> { - return grid.getWidth() / 2.5; - }, grid.widthProperty())); + name.maxWidthProperty() + .bind(Bindings.createDoubleBinding( + () -> { + return grid.getWidth() / 2.5; + }, + grid.widthProperty())); if (showIcon) { var storeIcon = createIcon(30, 24); diff --git a/app/src/main/java/io/xpipe/app/comp/store/StandardStoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/store/StandardStoreEntryComp.java index 6e402c56..e6b4e72c 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StandardStoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StandardStoreEntryComp.java @@ -12,7 +12,6 @@ public class StandardStoreEntryComp extends StoreEntryComp { super(entry, content); } - protected Region createContent() { var name = createName().createRegion(); diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java b/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java index 3755e801..d09c643b 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java @@ -1,9 +1,9 @@ package io.xpipe.app.comp.store; import io.xpipe.app.fxcomps.util.PlatformThread; +import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreCategory; -import io.xpipe.app.storage.DataStoreEntry; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; @@ -59,13 +59,13 @@ public class StoreCategoryWrapper { public StoreCategoryWrapper getParent() { return StoreViewState.get().getCategories().stream() .filter(storeCategoryWrapper -> - storeCategoryWrapper.getCategory().getUuid().equals(category.getParentCategory())) - .findAny().orElse(null); + storeCategoryWrapper.getCategory().getUuid().equals(category.getParentCategory())) + .findAny() + .orElse(null); } - public boolean contains(DataStoreEntry entry) { - return entry.getCategoryUuid().equals(category.getUuid()) - || children.stream().anyMatch(storeCategoryWrapper -> storeCategoryWrapper.contains(entry)); + public boolean contains(StoreEntryWrapper entry) { + return entry.getEntry().getCategoryUuid().equals(category.getUuid()) || containedEntries.contains(entry); } public void select() { @@ -87,6 +87,10 @@ public class StoreCategoryWrapper { update(); })); + AppPrefs.get().showChildCategoriesInParentCategory().addListener((observable, oldValue, newValue) -> { + update(); + }); + sortMode.addListener((observable, oldValue, newValue) -> { category.setSortMode(newValue); }); @@ -97,8 +101,8 @@ public class StoreCategoryWrapper { DataStoreCategory p = category; if (newValue) { while ((p = DataStorage.get() - .getStoreCategoryIfPresent(p.getParentCategory()) - .orElse(null)) + .getStoreCategoryIfPresent(p.getParentCategory()) + .orElse(null)) != null) { p.setShare(true); } @@ -117,17 +121,23 @@ public class StoreCategoryWrapper { share.setValue(category.isShare()); containedEntries.setAll(StoreViewState.get().getAllEntries().stream() - .filter(entry -> contains(entry.getEntry())) + .filter(entry -> { + return entry.getEntry().getCategoryUuid().equals(category.getUuid()) + || (AppPrefs.get() + .showChildCategoriesInParentCategory() + .get() + && children.stream() + .anyMatch(storeCategoryWrapper -> storeCategoryWrapper.contains(entry))); + }) .toList()); children.setAll(StoreViewState.get().getCategories().stream() .filter(storeCategoryWrapper -> getCategory() .getUuid() .equals(storeCategoryWrapper.getCategory().getParentCategory())) .toList()); - Optional.ofNullable(getParent()) - .ifPresent(storeCategoryWrapper -> { - storeCategoryWrapper.update(); - }); + Optional.ofNullable(getParent()).ifPresent(storeCategoryWrapper -> { + storeCategoryWrapper.update(); + }); } public String getName() { 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 e33003bb..59c2d5fd 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,15 @@ package io.xpipe.app.comp.store; +import atlantafx.base.controls.Spacer; +import io.xpipe.app.comp.base.ButtonComp; +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.ext.DataStoreProviders; 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 +27,12 @@ import javafx.beans.binding.Bindings; import javafx.beans.property.*; import javafx.beans.value.ObservableValue; import javafx.geometry.Insets; -import javafx.scene.control.Alert; -import javafx.scene.control.ScrollPane; -import javafx.scene.control.Separator; +import javafx.geometry.Orientation; +import javafx.scene.control.*; 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; @@ -53,19 +56,22 @@ public class StoreCreationComp extends MultiStepComp.Step> { BooleanProperty finished = new SimpleBooleanProperty(); ObservableValue entry; BooleanProperty changedSinceError = new SimpleBooleanProperty(); + BooleanProperty skippable = new SimpleBooleanProperty(); StringProperty name; DataStoreEntry existingEntry; 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; @@ -97,36 +103,42 @@ public class StoreCreationComp extends MultiStepComp.Step> { newValue.validate(); }); }); - this.entry = Bindings.createObjectBinding(() -> { - if (name.getValue() == null || store.getValue() == null) { - return null; - } + this.entry = Bindings.createObjectBinding( + () -> { + if (name.getValue() == null || store.getValue() == null) { + return null; + } - var testE = DataStoreEntry.createNew( - UUID.randomUUID(), - DataStorage.get().getSelectedCategory().getUuid(), - name.getValue(), - store.getValue()); - var p = provider.getValue().getDisplayParent(testE); + var testE = DataStoreEntry.createNew( + UUID.randomUUID(), + DataStorage.get().getSelectedCategory().getUuid(), + name.getValue(), + store.getValue()); + var p = provider.getValue().getDisplayParent(testE); - var targetCategory = p != null - ? p.getCategoryUuid() - : DataStorage.get() - .getSelectedCategory() - .getUuid(); - var rootCategory = DataStorage.get().getRootCategory(DataStorage.get().getStoreCategoryIfPresent(targetCategory).orElseThrow()); - // Don't put connections in the scripts category ever - if ((provider.getValue().getCreationCategory() == null || !provider.getValue().getCreationCategory().equals(DataStoreProvider.CreationCategory.SCRIPT)) && - rootCategory.equals(DataStorage.get().getAllScriptsCategory())) { - targetCategory = DataStorage.get().getDefaultCategory().getUuid(); - } + var targetCategory = p != null + ? p.getCategoryUuid() + : DataStorage.get().getSelectedCategory().getUuid(); + var rootCategory = DataStorage.get() + .getRootCategory(DataStorage.get() + .getStoreCategoryIfPresent(targetCategory) + .orElseThrow()); + // Don't put connections in the scripts category ever + if ((provider.getValue().getCreationCategory() == null + || !provider.getValue() + .getCreationCategory() + .equals(DataStoreProvider.CreationCategory.SCRIPT)) + && rootCategory.equals(DataStorage.get().getAllScriptsCategory())) { + targetCategory = DataStorage.get() + .getDefaultConnectionsCategory() + .getUuid(); + } - return DataStoreEntry.createNew( - UUID.randomUUID(), - targetCategory, - name.getValue(), - store.getValue()); - }, name, store); + return DataStoreEntry.createNew( + UUID.randomUUID(), targetCategory, name.getValue(), store.getValue()); + }, + name, + store); } public static void showEdit(DataStoreEntry e) { @@ -148,16 +160,23 @@ public class StoreCreationComp extends MultiStepComp.Step> { e); } - public static void showCreation(DataStoreProvider selected, Predicate filter) { + public static void showCreation(DataStoreProvider selected, DataStoreProvider.CreationCategory category) { + showCreation(selected != null ? selected.defaultStore() : null, category); + } + + public static void showCreation(DataStore base, DataStoreProvider.CreationCategory category) { show( null, - selected, - selected != null ? selected.defaultStore() : null, - filter, + base != null ? DataStoreProviders.byStore(base) : null, + base, + dataStoreProvider -> category.equals(dataStoreProvider.getCreationCategory()), e -> { try { DataStorage.get().addStoreEntryIfNotPresent(e); - if (e.getProvider().shouldHaveChildren()) { + if (e.getProvider().shouldHaveChildren() + && AppPrefs.get() + .openConnectionSearchWindowOnConnectionCreation() + .get()) { ScanAlert.showAsync(e); } } catch (Exception ex) { @@ -178,38 +197,117 @@ 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); + private static boolean showInvalidConfirmAlert() { + return AppWindowHelper.showBlockingAlert(alert -> { + alert.setTitle(AppI18n.get("confirmInvalidStoreTitle")); + alert.setHeaderText(AppI18n.get("confirmInvalidStoreHeader")); + alert.getDialogPane() + .setContent(AppWindowHelper.alertContentText(AppI18n.get("confirmInvalidStoreContent"))); + alert.setAlertType(Alert.AlertType.CONFIRMATION); + alert.getButtonTypes().clear(); + alert.getButtonTypes().add(new ButtonType("Retry", ButtonBar.ButtonData.CANCEL_CLOSE)); + alert.getButtonTypes().add(new ButtonType("Skip", ButtonBar.ButtonData.OK_DONE)); + }) + .map(b -> b.getButtonData().isDefaultButton()) + .orElse(false); + } - @Override - protected List setup() { - loading.bind(creator.busy); - return List.of(new Entry(AppI18n.observable("a"), creator)); - } + @Override + protected List> customButtons() { + return List.of(new ButtonComp(AppI18n.observable("skip"), null, () -> { + if (showInvalidConfirmAlert()) { + commit(); + } else { + finish(); + } + }) + .visible(skippable)); + } - @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 + 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 (!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) { + if (ex instanceof ValidationException) { + ErrorEvent.unreportable(ex); + skippable.set(false); + } else { + skippable.set(true); + } + + 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); + + ErrorEvent.fromThrowable(ex).omit().handle(); + } finally { + DataStorage.get().removeStoreEntryInProgress(entry.getValue()); + } }); } + @Override + public Comp content() { + return Comp.of(this::createLayout); + } + + @Override + protected Comp scrollPane(Comp content) { + var back = super.scrollPane(content); + return new ErrorOverlayComp(back, messageProp); + } + @Override public Comp bottom() { var disable = Bindings.createBooleanBinding( @@ -219,7 +317,9 @@ public class StoreCreationComp extends MultiStepComp.Step> { || !store.getValue().isComplete() // When switching providers, both observables change one after another. // So temporarily there might be a store class mismatch - || provider.getValue().getStoreClasses().stream().noneMatch(aClass -> aClass.isAssignableFrom(store.getValue().getClass())) + || provider.getValue().getStoreClasses().stream() + .noneMatch(aClass -> aClass.isAssignableFrom( + store.getValue().getClass())) || provider.getValue().createInsightsMarkdown(store.getValue()) == null; }, provider, @@ -233,22 +333,11 @@ public class StoreCreationComp extends MultiStepComp.Step> { .createRegion() : null; }), - true) + true) .hide(disable) .styleClass("button-comp"); } - private static boolean showInvalidConfirmAlert() { - return AppWindowHelper.showBlockingAlert(alert -> { - alert.setTitle(AppI18n.get("confirmInvalidStoreTitle")); - alert.setHeaderText(AppI18n.get("confirmInvalidStoreHeader")); - alert.setContentText(AppI18n.get("confirmInvalidStoreContent")); - alert.setAlertType(Alert.AlertType.CONFIRMATION); - }) - .map(b -> b.getButtonData().isDefaultButton()) - .orElse(false); - } - private Region createStoreProperties(Comp comp, Validator propVal) { return new OptionsBuilder() .addComp(comp, store) @@ -259,18 +348,26 @@ public class StoreCreationComp extends MultiStepComp.Step> { .build(); } - @Override - public CompStructure createBase() { - var back = Comp.of(this::createLayout); - var message = new ErrorOverlayComp(back, messageProp); - return message.createStructure(); + private void commit() { + if (finished.get()) { + return; + } + finished.setValue(true); + + if (entry.getValue() != null) { + consumer.accept(entry.getValue()); + } + + PlatformThread.runLaterIfNeeded(() -> { + window.close(); + }); } 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 +394,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/StoreCreationMenu.java b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java index e2c7734e..b206ff88 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java @@ -24,37 +24,42 @@ public class StoreCreationMenu { menu.getItems().add(automatically); menu.getItems().add(new SeparatorMenuItem()); - menu.getItems().add(category("addHost", "mdi2h-home-plus", - DataStoreProvider.CreationCategory.HOST, "ssh")); + menu.getItems().add(category("addHost", "mdi2h-home-plus", DataStoreProvider.CreationCategory.HOST, "ssh")); - menu.getItems().add(category("addShell", "mdi2t-text-box-multiple", - DataStoreProvider.CreationCategory.SHELL, null)); + menu.getItems() + .add(category("addShell", "mdi2t-text-box-multiple", DataStoreProvider.CreationCategory.SHELL, null)); - menu.getItems().add(category("addScript", "mdi2s-script-text-outline", - DataStoreProvider.CreationCategory.SCRIPT, "script")); + menu.getItems() + .add(category( + "addScript", "mdi2s-script-text-outline", DataStoreProvider.CreationCategory.SCRIPT, "script")); - menu.getItems().add(category("addCommand", "mdi2c-code-greater-than", - DataStoreProvider.CreationCategory.COMMAND, "cmd")); + menu.getItems() + .add(category( + "addCommand", "mdi2c-code-greater-than", DataStoreProvider.CreationCategory.COMMAND, "cmd")); - menu.getItems().add(category("addTunnel", "mdi2v-vector-polyline-plus", - DataStoreProvider.CreationCategory.TUNNEL, null)); + menu.getItems() + .add(category( + "addTunnel", "mdi2v-vector-polyline-plus", DataStoreProvider.CreationCategory.TUNNEL, null)); - menu.getItems().add(category("addCluster", "mdi2d-domain-plus", - DataStoreProvider.CreationCategory.CLUSTER, null)); - - menu.getItems().add(category("addDatabase", "mdi2d-database-plus", - DataStoreProvider.CreationCategory.DATABASE, null)); + menu.getItems() + .add(category("addDatabase", "mdi2d-database-plus", DataStoreProvider.CreationCategory.DATABASE, null)); } - private static MenuItem category(String name, String graphic, DataStoreProvider.CreationCategory category, String defaultProvider) { - var sub = DataStoreProviders.getAll().stream().filter(dataStoreProvider -> category.equals(dataStoreProvider.getCreationCategory())).toList(); + private static MenuItem category( + String name, String graphic, DataStoreProvider.CreationCategory category, String defaultProvider) { + var sub = DataStoreProviders.getAll().stream() + .filter(dataStoreProvider -> category.equals(dataStoreProvider.getCreationCategory())) + .toList(); if (sub.size() < 2) { var item = new MenuItem(); item.setGraphic(new FontIcon(graphic)); item.textProperty().bind(AppI18n.observable(name)); item.setOnAction(event -> { - StoreCreationComp.showCreation(defaultProvider != null ? DataStoreProviders.byName(defaultProvider).orElseThrow() : null, - v -> category.equals(v.getCreationCategory())); + StoreCreationComp.showCreation( + defaultProvider != null + ? DataStoreProviders.byName(defaultProvider).orElseThrow() + : null, + category); event.consume(); }); return item; @@ -68,16 +73,19 @@ public class StoreCreationMenu { return; } - StoreCreationComp.showCreation(defaultProvider != null ? DataStoreProviders.byName(defaultProvider).orElseThrow() : null, - v -> category.equals(v.getCreationCategory())); + StoreCreationComp.showCreation( + defaultProvider != null + ? DataStoreProviders.byName(defaultProvider).orElseThrow() + : null, + category); event.consume(); }); sub.forEach(dataStoreProvider -> { var item = new MenuItem(dataStoreProvider.getDisplayName()); - item.setGraphic(PrettyImageHelper.ofFixedSmallSquare(dataStoreProvider.getDisplayIconFileName(null)).createRegion()); + item.setGraphic(PrettyImageHelper.ofFixedSmallSquare(dataStoreProvider.getDisplayIconFileName(null)) + .createRegion()); item.setOnAction(event -> { - StoreCreationComp.showCreation(dataStoreProvider, - v -> category.equals(v.getCreationCategory())); + StoreCreationComp.showCreation(dataStoreProvider, category); event.consume(); }); menu.getItems().add(item); 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 6ffc7296..386ef456 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 @@ -40,24 +40,6 @@ import java.util.Arrays; public abstract class StoreEntryComp extends SimpleComp { - public static StoreEntryComp create( - StoreEntryWrapper entry, Comp content, boolean preferLarge) { - if (!preferLarge) { - return new DenseStoreEntryComp(entry, true, content); - } else { - return new StandardStoreEntryComp(entry, content); - } - } - - public static Comp customSection(StoreSection e, boolean topLevel) { - var prov = e.getWrapper().getEntry().getProvider(); - if (prov != null) { - return prov.customEntryComp(e, topLevel); - } else { - return new StandardStoreEntryComp(e.getWrapper(), null); - } - } - public static final PseudoClass FAILED = PseudoClass.getPseudoClass("failed"); public static final PseudoClass INCOMPLETE = PseudoClass.getPseudoClass("incomplete"); public static final ObservableDoubleValue INFO_NO_CONTENT_WIDTH = @@ -72,6 +54,29 @@ public abstract class StoreEntryComp extends SimpleComp { this.content = content; } + public static StoreEntryComp create(StoreEntryWrapper entry, Comp content, boolean 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); + } + } + + public static Comp customSection(StoreSection e, boolean topLevel) { + var prov = e.getWrapper().getEntry().getProvider(); + if (prov != null) { + return prov.customEntryComp(e, topLevel); + } else { + var forceCondensed = AppPrefs.get() != null + && AppPrefs.get().condenseConnectionDisplay().get(); + return forceCondensed + ? new DenseStoreEntryComp(e.getWrapper(), true, null) + : new StandardStoreEntryComp(e.getWrapper(), null); + } + } + @Override protected final Region createSimple() { var r = createContent(); @@ -83,8 +88,7 @@ public abstract class StoreEntryComp extends SimpleComp { button.setPadding(Insets.EMPTY); button.setMaxWidth(5000); button.setFocusTraversable(true); - button.accessibleTextProperty() - .bind(wrapper.nameProperty()); + button.accessibleTextProperty().bind(wrapper.nameProperty()); button.setOnAction(event -> { event.consume(); ThreadHelper.runFailableAsync(() -> { @@ -105,8 +109,13 @@ public abstract class StoreEntryComp extends SimpleComp { protected Label createInformation() { var information = new Label(); information.setGraphicTextGap(7); - information.textProperty().bind(wrapper.getEntry().getProvider() != null ? - PlatformThread.sync(wrapper.getEntry().getProvider().informationString(wrapper)) : new SimpleStringProperty()); + information + .textProperty() + .bind( + wrapper.getEntry().getProvider() != null + ? PlatformThread.sync( + wrapper.getEntry().getProvider().informationString(wrapper)) + : new SimpleStringProperty()); information.getStyleClass().add("information"); AppFont.header(information); @@ -191,15 +200,16 @@ public abstract class StoreEntryComp extends SimpleComp { continue; } - var button = new IconButtonComp( - actionProvider.getIcon(wrapper.getEntry().ref()), () -> { + var button = + new IconButtonComp(actionProvider.getIcon(wrapper.getEntry().ref()), () -> { ThreadHelper.runFailableAsync(() -> { var action = actionProvider.createAction( wrapper.getEntry().ref()); action.execute(); }); }); - button.accessibleText(actionProvider.getName(wrapper.getEntry().ref()).getValue()); + button.accessibleText( + actionProvider.getName(wrapper.getEntry().ref()).getValue()); button.apply(new FancyTooltipAugment<>( actionProvider.getName(wrapper.getEntry().ref()))); if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ONLY_SHOW_IF_ENABLED) { @@ -213,11 +223,11 @@ public abstract class StoreEntryComp extends SimpleComp { var settingsButton = createSettingsButton(); list.add(settingsButton); if (list.size() > 1) { - list.get(0).styleClass(Styles.LEFT_PILL); + list.getFirst().styleClass(Styles.LEFT_PILL); for (int i = 1; i < list.size() - 1; i++) { list.get(i).styleClass(Styles.CENTER_PILL); } - list.get(list.size() - 1).styleClass(Styles.RIGHT_PILL); + list.getLast().styleClass(Styles.RIGHT_PILL); } list.forEach(comp -> { comp.apply(struc -> struc.get().getStyleClass().remove(Styles.FLAT)); @@ -264,11 +274,13 @@ public abstract class StoreEntryComp extends SimpleComp { ? new Menu(null, new FontIcon(icon)) : new MenuItem(null, new FontIcon(icon)); - var proRequired = p.getKey().getProFeatureId() != null && - !LicenseProvider.get().getFeature(p.getKey().getProFeatureId()).isSupported(); + var proRequired = p.getKey().getProFeatureId() != null + && !LicenseProvider.get() + .getFeature(p.getKey().getProFeatureId()) + .isSupported(); if (proRequired) { item.setDisable(true); - item.textProperty().bind(Bindings.createStringBinding(() -> name.getValue() + " (Pro)",name)); + item.textProperty().bind(Bindings.createStringBinding(() -> name.getValue() + " (Pro)", name)); } else { item.textProperty().bind(name); } @@ -285,8 +297,7 @@ public abstract class StoreEntryComp extends SimpleComp { contextMenu.hide(); ThreadHelper.runFailableAsync(() -> { - var action = actionProvider.createAction( - wrapper.getEntry().ref()); + var action = actionProvider.createAction(wrapper.getEntry().ref()); action.execute(); }); }); @@ -302,20 +313,27 @@ public abstract class StoreEntryComp extends SimpleComp { run.textProperty().bind(AppI18n.observable("base.execute")); run.setOnAction(event -> { ThreadHelper.runFailableAsync(() -> { - p.getKey().getDataStoreCallSite().createAction(wrapper.getEntry().ref()).execute(); + p.getKey() + .getDataStoreCallSite() + .createAction(wrapper.getEntry().ref()) + .execute(); }); }); menu.getItems().add(run); - var sc = new MenuItem(null, new FontIcon("mdi2c-code-greater-than")); var url = "xpipe://action/" + p.getKey().getId() + "/" + wrapper.getEntry().getUuid(); sc.textProperty().bind(AppI18n.observable("base.createShortcut")); sc.setOnAction(event -> { ThreadHelper.runFailableAsync(() -> { - DesktopShortcuts.create(url, - wrapper.nameProperty().getValue() + " (" + p.getKey().getDataStoreCallSite().getName(wrapper.getEntry().ref()).getValue() + ")"); + DesktopShortcuts.create( + url, + wrapper.nameProperty().getValue() + " (" + + p.getKey() + .getDataStoreCallSite() + .getName(wrapper.getEntry().ref()) + .getValue() + ")"); }); }); menu.getItems().add(sc); @@ -345,20 +363,23 @@ public abstract class StoreEntryComp extends SimpleComp { contextMenu.getItems().add(browse); } - if (wrapper.getEntry().getProvider() != null && wrapper.getEntry().getProvider().canMoveCategories()) { + if (wrapper.getEntry().getProvider() != null + && wrapper.getEntry().getProvider().canMoveCategories()) { var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline")); - StoreViewState.get().getSortedCategories(wrapper.getCategory().getValue().getRoot()).forEach(storeCategoryWrapper -> { - MenuItem m = new MenuItem(storeCategoryWrapper.getName()); - m.setOnAction(event -> { - wrapper.moveTo(storeCategoryWrapper.getCategory()); - event.consume(); - }); - if (storeCategoryWrapper.getParent() == null) { - m.setDisable(true); - } + StoreViewState.get() + .getSortedCategories(wrapper.getCategory().getValue().getRoot()) + .forEach(storeCategoryWrapper -> { + MenuItem m = new MenuItem(storeCategoryWrapper.getName()); + m.setOnAction(event -> { + wrapper.moveTo(storeCategoryWrapper.getCategory()); + event.consume(); + }); + if (storeCategoryWrapper.getParent() == null) { + m.setDisable(true); + } - move.getItems().add(m); - }); + move.getItems().add(m); + }); contextMenu.getItems().add(move); } @@ -382,9 +403,16 @@ public abstract class StoreEntryComp extends SimpleComp { } var del = new MenuItem(AppI18n.get("remove"), new FontIcon("mdal-delete_outline")); - del.disableProperty().bind(Bindings.createBooleanBinding(() -> { - return !wrapper.getDeletable().get() && !AppPrefs.get().developerDisableGuiRestrictions().get(); - }, wrapper.getDeletable(), AppPrefs.get().developerDisableGuiRestrictions())); + del.disableProperty() + .bind(Bindings.createBooleanBinding( + () -> { + return !wrapper.getDeletable().get() + && !AppPrefs.get() + .developerDisableGuiRestrictions() + .get(); + }, + wrapper.getDeletable(), + AppPrefs.get().developerDisableGuiRestrictions())); del.setOnAction(event -> wrapper.delete()); contextMenu.getItems().add(del); diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListComp.java index 72d5a00f..2973a396 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListComp.java @@ -35,10 +35,18 @@ public class StoreEntryListComp extends SimpleComp { var showIntro = Bindings.createBooleanBinding( () -> { var all = StoreViewState.get().getAllConnectionsCategory(); - var connections = StoreViewState.get().getAllEntries().stream().filter(wrapper -> all.contains(wrapper.getEntry())).toList(); - return initialCount == connections.size() && StoreViewState.get().getActiveCategory().getValue().getRoot().equals(StoreViewState.get().getAllConnectionsCategory()); + var connections = StoreViewState.get().getAllEntries().stream() + .filter(wrapper -> all.contains(wrapper)) + .toList(); + return initialCount == connections.size() + && StoreViewState.get() + .getActiveCategory() + .getValue() + .getRoot() + .equals(StoreViewState.get().getAllConnectionsCategory()); }, - StoreViewState.get().getAllEntries(), StoreViewState.get().getActiveCategory()); + StoreViewState.get().getAllEntries(), + StoreViewState.get().getActiveCategory()); var map = new LinkedHashMap, ObservableValue>(); map.put( createList(), diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListStatusComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListStatusComp.java index 24181a25..5eb91dee 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListStatusComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListStatusComp.java @@ -37,23 +37,43 @@ public class StoreEntryListStatusComp extends SimpleComp { public StoreEntryListStatusComp() { this.sortMode = new SimpleObjectProperty<>(); SimpleChangeListener.apply(StoreViewState.get().getActiveCategory(), val -> { - sortMode.unbind(); - sortMode.bindBidirectional(val.getSortMode()); + sortMode.setValue(val.getSortMode().getValue()); + }); + sortMode.addListener((observable, oldValue, newValue) -> { + var cat = StoreViewState.get().getActiveCategory().getValue(); + if (cat == null) { + return; + } + + cat.getSortMode().setValue(newValue); }); } private Region createGroupListHeader() { var label = new Label(); - label.textProperty().bind(Bindings.createStringBinding(() -> { - return StoreViewState.get().getActiveCategory().getValue().getRoot().equals(StoreViewState.get().getAllConnectionsCategory()) ? "Connections" : "Scripts"; - }, StoreViewState.get().getActiveCategory())); + label.textProperty() + .bind(Bindings.createStringBinding( + () -> { + return StoreViewState.get() + .getActiveCategory() + .getValue() + .getRoot() + .equals(StoreViewState.get().getAllConnectionsCategory()) + ? "Connections" + : "Scripts"; + }, + StoreViewState.get().getActiveCategory())); label.getStyleClass().add("name"); var all = BindingsHelper.filteredContentBinding( StoreViewState.get().getAllEntries(), storeEntryWrapper -> { var storeRoot = storeEntryWrapper.getCategory().getValue().getRoot(); - return StoreViewState.get().getActiveCategory().getValue().getRoot().equals(storeRoot); + return StoreViewState.get() + .getActiveCategory() + .getValue() + .getRoot() + .equals(storeRoot); }, StoreViewState.get().getActiveCategory()); var shownList = BindingsHelper.filteredContentBinding( @@ -66,7 +86,13 @@ public class StoreEntryListStatusComp extends SimpleComp { var count = new CountComp<>(shownList, all); var c = count.createRegion(); - var topBar = new HBox(label, c, Comp.hspacer().createRegion(), createDateSortButton().createRegion(), Comp.hspacer(2).createRegion(), createAlphabeticalSortButton().createRegion()); + var topBar = new HBox( + label, + c, + Comp.hspacer().createRegion(), + createDateSortButton().createRegion(), + Comp.hspacer(2).createRegion(), + createAlphabeticalSortButton().createRegion()); AppFont.setSize(label, 3); AppFont.setSize(c, 3); topBar.setAlignment(Pos.CENTER); @@ -87,7 +113,7 @@ public class StoreEntryListStatusComp extends SimpleComp { }); filter.apply(struc -> struc.get().sceneProperty().addListener((observable, oldValue, newValue) -> { if (newValue != null) { - struc.getText().requestFocus(); + struc.getText().requestFocus(); } })); @@ -104,7 +130,6 @@ public class StoreEntryListStatusComp extends SimpleComp { f.setPadding(new Insets(-3, 0, -3, 0)); } - AppFont.medium(hbox); return hbox; } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryWrapper.java b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryWrapper.java index c8f69332..874e8d1e 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryWrapper.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryWrapper.java @@ -121,7 +121,10 @@ public class StoreEntryWrapper { deletable.setValue(entry.getConfiguration().isDeletable() || AppPrefs.get().developerDisableGuiRestrictions().getValue()); - category.setValue(StoreViewState.get().getCategoryWrapper(DataStorage.get().getStoreCategoryIfPresent(entry.getCategoryUuid()).orElseThrow())); + category.setValue(StoreViewState.get() + .getCategoryWrapper(DataStorage.get() + .getStoreCategoryIfPresent(entry.getCategoryUuid()) + .orElseThrow())); if (!entry.getValidity().isUsable()) { summary.setValue(null); @@ -155,8 +158,7 @@ public class StoreEntryWrapper { && e.getDefaultDataStoreCallSite() .getApplicableClass() .isAssignableFrom(entry.getStore().getClass()) - && e.getDefaultDataStoreCallSite() - .isApplicable(entry.ref())) + && e.getDefaultDataStoreCallSite().isApplicable(entry.ref())) .findFirst() .map(ActionProvider::getDefaultDataStoreCallSite) .orElse(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 b85d8c91..e508b37f 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,23 @@ 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 img = PrettyImageHelper.ofSvg(new SimpleStringProperty("Wave.svg"), 80, 150) + .createRegion(); + 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/StoreLayoutComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreLayoutComp.java index 39411d2f..e31bd57d 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreLayoutComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreLayoutComp.java @@ -19,10 +19,12 @@ public class StoreLayoutComp extends SimpleComp { @Override protected Region createSimple() { - var struc = new SideSplitPaneComp(new StoreSidebarComp(), new StoreEntryListComp()).withInitialWidth( - AppLayoutModel.get().getSavedState().getSidebarWidth()).withOnDividerChange(aDouble -> { - AppLayoutModel.get().getSavedState().setSidebarWidth(aDouble); - }).createStructure(); + var struc = new SideSplitPaneComp(new StoreSidebarComp(), new StoreEntryListComp()) + .withInitialWidth(AppLayoutModel.get().getSavedState().getSidebarWidth()) + .withOnDividerChange(aDouble -> { + AppLayoutModel.get().getSavedState().setSidebarWidth(aDouble); + }) + .createStructure(); struc.getLeft().setMinWidth(260); struc.getLeft().setMaxWidth(500); struc.get().getStyleClass().add("store-layout"); diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreProviderChoiceComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreProviderChoiceComp.java new file mode 100644 index 00000000..e9af0f35 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreProviderChoiceComp.java @@ -0,0 +1,82 @@ +package io.xpipe.app.comp.store; + +import io.xpipe.app.ext.DataStoreProvider; +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.util.JfxHelper; +import javafx.beans.property.Property; +import javafx.scene.control.ComboBox; +import javafx.scene.control.ListCell; +import javafx.scene.input.KeyCode; +import javafx.scene.layout.Region; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.experimental.FieldDefaults; + +import java.util.List; +import java.util.function.Predicate; +import java.util.function.Supplier; + +@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) +@AllArgsConstructor +public class StoreProviderChoiceComp extends Comp>> { + + Predicate filter; + Property provider; + boolean staticDisplay; + + private List getProviders() { + return DataStoreProviders.getAll().stream() + .filter(val -> filter == null || filter.test(val)) + .toList(); + } + + private Region createGraphic(DataStoreProvider provider) { + if (provider == null) { + return null; + } + + var graphic = provider.getDisplayIconFileName(null); + return JfxHelper.createNamedEntry(provider.getDisplayName(), provider.getDisplayDescription(), graphic); + } + + @Override + public CompStructure> createBase() { + Supplier> cellFactory = () -> new ListCell<>() { + @Override + protected void updateItem(DataStoreProvider item, boolean empty) { + super.updateItem(item, empty); + setGraphic(createGraphic(item)); + setAccessibleText(item != null ? item.getDisplayName() : null); + setAccessibleHelp(item != null ? item.getDisplayDescription() : null); + } + }; + var cb = new ComboBox(); + cb.setCellFactory(param -> { + return cellFactory.get(); + }); + cb.setButtonCell(cellFactory.get()); + var l = getProviders().stream() + .filter(p -> p.getCreationCategory() != null || staticDisplay) + .toList(); + l.forEach(dataStoreProvider -> cb.getItems().add(dataStoreProvider)); + if (provider.getValue() == null) { + provider.setValue(l.getFirst()); + } + cb.setValue(provider.getValue()); + provider.bind(cb.valueProperty()); + cb.getStyleClass().add("choice-comp"); + cb.setAccessibleText("Choose connection type"); + cb.setOnKeyPressed(event -> { + if (!event.getCode().equals(KeyCode.ENTER)) { + return; + } + + cb.show(); + event.consume(); + }); + return new SimpleCompStructure<>(cb); + } +} 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 15c95390..a2e68542 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 @@ -19,15 +19,6 @@ import java.util.function.Predicate; @Value public class StoreSection { - public static Comp customSection(StoreSection e, boolean topLevel) { - var prov = e.getWrapper().getEntry().getProvider(); - if (prov != null) { - return prov.customSectionComp(e, topLevel); - } else { - return new StoreSectionComp(e, topLevel); - } - } - StoreEntryWrapper wrapper; ObservableList allChildren; ObservableList shownChildren; @@ -55,6 +46,15 @@ public class StoreSection { } } + public static Comp customSection(StoreSection e, boolean topLevel) { + var prov = e.getWrapper().getEntry().getProvider(); + if (prov != null) { + return prov.customSectionComp(e, topLevel); + } else { + return new StoreSectionComp(e, topLevel); + } + } + private static ObservableList sorted( ObservableList list, ObservableValue category) { if (category == null) { @@ -63,14 +63,16 @@ public class StoreSection { var c = Comparator.comparingInt( value -> value.getWrapper().getEntry().getValidity().isUsable() ? -1 : 1); - var mappedSortMode = BindingsHelper.mappedBinding(category, storeCategoryWrapper -> storeCategoryWrapper != null ? storeCategoryWrapper.getSortMode() : null); + var mappedSortMode = BindingsHelper.mappedBinding( + category, + storeCategoryWrapper -> storeCategoryWrapper != null ? storeCategoryWrapper.getSortMode() : null); return BindingsHelper.orderedContentBinding( list, (o1, o2) -> { 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); } @@ -97,7 +99,9 @@ public class StoreSection { section -> { var showFilter = filterString == null || section.shouldShow(filterString.get()); var matchesSelector = section.anyMatches(entryFilter); - var sameCategory = category == null || category.getValue() == null || category.getValue().contains(section.getWrapper().getEntry()); + var sameCategory = category == null + || category.getValue() == null + || category.getValue().contains(section.getWrapper()); return showFilter && matchesSelector && sameCategory; }, category, @@ -117,11 +121,11 @@ public class StoreSection { } var allChildren = BindingsHelper.filteredContentBinding(all, other -> { - // Legacy implementation that does not use caches. Use for testing -// if (true) return DataStorage.get() -// .getDisplayParent(other.getEntry()) -// .map(found -> found.equals(e.getEntry())) -// .orElse(false); + // Legacy implementation that does not use children caches. Use for testing + // if (true) return DataStorage.get() + // .getDisplayParent(other.getEntry()) + // .map(found -> found.equals(e.getEntry())) + // .orElse(false); // This check is fast as the children are cached in the storage return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry()); @@ -134,9 +138,13 @@ public class StoreSection { section -> { var showFilter = filterString == null || section.shouldShow(filterString.get()); var matchesSelector = section.anyMatches(entryFilter); - var sameCategory = category == null || category.getValue() == null || category.getValue().contains(section.getWrapper().getEntry()); - // If this entry is already shown as root due to a different category than parent, don't show it again here - var notRoot = !DataStorage.get().isRootEntry(section.getWrapper().getEntry()); + var sameCategory = category == null + || category.getValue() == null + || category.getValue().contains(section.getWrapper()); + // If this entry is already shown as root due to a different category than parent, don't show it + // again here + var notRoot = + !DataStorage.get().isRootEntry(section.getWrapper().getEntry()); return showFilter && matchesSelector && sameCategory && notRoot; }, category, 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 ac4ae2ae..531c6661 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 @@ -22,12 +22,11 @@ import java.util.List; public class StoreSectionComp extends Comp> { + public static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded"); private static final PseudoClass ROOT = PseudoClass.getPseudoClass("root"); private static final PseudoClass SUB = PseudoClass.getPseudoClass("sub"); private static final PseudoClass ODD = PseudoClass.getPseudoClass("odd-depth"); private static final PseudoClass EVEN = PseudoClass.getPseudoClass("even-depth"); - public static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded"); - private final StoreSection section; private final boolean topLevel; @@ -38,7 +37,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( @@ -54,9 +53,11 @@ public class StoreSectionComp extends Comp> { .apply(struc -> struc.get().setMinWidth(30)) .apply(struc -> struc.get().setPrefWidth(30)) .focusTraversable() - .accessibleText(Bindings.createStringBinding(() -> { - return "Expand " + section.getWrapper().getName().getValue(); - }, section.getWrapper().getName())) + .accessibleText(Bindings.createStringBinding( + () -> { + return "Expand " + section.getWrapper().getName().getValue(); + }, + section.getWrapper().getName())) .disable(BindingsHelper.persist( Bindings.size(section.getShownChildren()).isEqualTo(0))) .grow(false, true) diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java index 08cbd056..cb1388ca 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java @@ -28,19 +28,18 @@ import java.util.function.BiConsumer; @Builder public class StoreSectionMiniComp extends Comp> { - public static Comp createList(StoreSection top, BiConsumer>> augment) { - return new StoreSectionMiniComp(top, augment); - } - + public static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded"); private static final PseudoClass ODD = PseudoClass.getPseudoClass("odd-depth"); private static final PseudoClass EVEN = PseudoClass.getPseudoClass("even-depth"); - public static final PseudoClass EXPANDED = PseudoClass.getPseudoClass("expanded"); - private final StoreSection section; @Builder.Default private final BiConsumer>> augment = (section1, buttonComp) -> {}; + public static Comp createList(StoreSection top, BiConsumer>> augment) { + return new StoreSectionMiniComp(top, augment); + } + @Override public CompStructure createBase() { var list = new ArrayList>(); @@ -48,14 +47,14 @@ public class StoreSectionMiniComp extends Comp> { if (section.getWrapper() != null) { var root = new ButtonComp(section.getWrapper().nameProperty(), () -> {}) .apply(struc -> { - var provider = section.getWrapper() - .getEntry() - .getProvider(); + var provider = section.getWrapper().getEntry().getProvider(); struc.get() - .setGraphic(PrettyImageHelper.ofFixedSmallSquare(provider != null ? provider - .getDisplayIconFileName(section.getWrapper() - .getEntry() - .getStore()) : null) + .setGraphic(PrettyImageHelper.ofFixedSmallSquare( + provider != null + ? provider.getDisplayIconFileName(section.getWrapper() + .getEntry() + .getStore()) + : null) .createRegion()); }) .apply(struc -> { @@ -79,38 +78,47 @@ public class StoreSectionMiniComp extends Comp> { .apply(struc -> struc.get().setMinWidth(20)) .apply(struc -> struc.get().setPrefWidth(20)) .focusTraversable() - .accessibleText(Bindings.createStringBinding(() -> { - return "Expand " + section.getWrapper().getName().getValue(); - }, section.getWrapper().getName())) + .accessibleText(Bindings.createStringBinding( + () -> { + return "Expand " + + section.getWrapper().getName().getValue(); + }, + section.getWrapper().getName())) .disable(BindingsHelper.persist( Bindings.size(section.getAllChildren()).isEqualTo(0))) .grow(false, true) .styleClass("expand-button"); List> topEntryList = List.of(button, root); - list.add(new HorizontalComp(topEntryList) - .apply(struc -> struc.get().setFillHeight(true))); + list.add(new HorizontalComp(topEntryList).apply(struc -> struc.get().setFillHeight(true))); } else { expanded = new SimpleBooleanProperty(true); } // Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the // section is actually expanded - var listSections = section.getWrapper() != null ? BindingsHelper.filteredContentBinding( - section.getShownChildren(), - storeSection -> section.getAllChildren().size() <= 20 - || expanded.get(), - expanded, - section.getAllChildren()) : section.getShownChildren(); + var listSections = section.getWrapper() != null + ? BindingsHelper.filteredContentBinding( + section.getShownChildren(), + storeSection -> section.getAllChildren().size() <= 20 || expanded.get(), + expanded, + section.getAllChildren()) + : section.getShownChildren(); var content = new ListBoxViewComp<>(listSections, section.getAllChildren(), (StoreSection e) -> { - return StoreSectionMiniComp.builder().section(e).augment(this.augment).build(); - }).withLimit(100).minHeight(0).hgrow(); + return StoreSectionMiniComp.builder() + .section(e) + .augment(this.augment) + .build(); + }) + .withLimit(100) + .minHeight(0) + .hgrow(); list.add(new HorizontalComp(List.of(content)) - .styleClass("content") - .apply(struc -> struc.get().setFillHeight(true)) - .hide(BindingsHelper.persist(Bindings.or( - Bindings.not(expanded), - Bindings.size(section.getAllChildren()).isEqualTo(0))))); + .styleClass("content") + .apply(struc -> struc.get().setFillHeight(true)) + .hide(BindingsHelper.persist(Bindings.or( + Bindings.not(expanded), + Bindings.size(section.getAllChildren()).isEqualTo(0))))); return new VerticalComp(list) .styleClass("store-section-mini-comp") @@ -130,8 +138,9 @@ public class StoreSectionMiniComp extends Comp> { return; } - struc.get().getStyleClass().removeIf( - s -> Arrays.stream(DataStoreColor.values()).anyMatch(dataStoreColor -> dataStoreColor.getId().equals(s))); + struc.get().getStyleClass().removeIf(s -> Arrays.stream(DataStoreColor.values()) + .anyMatch(dataStoreColor -> + dataStoreColor.getId().equals(s))); struc.get().getStyleClass().remove("none"); struc.get().getStyleClass().add("color-box"); if (val != null) { @@ -141,7 +150,7 @@ public class StoreSectionMiniComp extends Comp> { } }); } - }) + }) .createStructure(); } } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreSidebarComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreSidebarComp.java index edccd1cf..107e35e9 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreSidebarComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreSidebarComp.java @@ -14,9 +14,18 @@ public class StoreSidebarComp extends SimpleComp { protected Region createSimple() { var sideBar = new VerticalComp(List.of( new StoreEntryListStatusComp().styleClass("color-box").styleClass("gray"), - new StoreCategoryListComp(StoreViewState.get().getAllConnectionsCategory()).styleClass("color-box").styleClass("gray"), - new StoreCategoryListComp(StoreViewState.get().getAllScriptsCategory()).styleClass("color-box").styleClass("gray"), - Comp.of(() -> new Region()).styleClass("bar").styleClass("color-box").styleClass("gray").styleClass("filler-bar").vgrow())); + new StoreCategoryListComp(StoreViewState.get().getAllConnectionsCategory()) + .styleClass("color-box") + .styleClass("gray"), + new StoreCategoryListComp(StoreViewState.get().getAllScriptsCategory()) + .styleClass("color-box") + .styleClass("gray"), + Comp.of(() -> new Region()) + .styleClass("bar") + .styleClass("color-box") + .styleClass("gray") + .styleClass("filler-bar") + .vgrow())); sideBar.apply(struc -> struc.get().setFillWidth(true)); sideBar.styleClass("sidebar"); sideBar.prefWidth(240); 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 4c43d2b3..d5a7c81c 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 @@ -12,6 +12,11 @@ import java.util.stream.Stream; public interface StoreSortMode { StoreSortMode ALPHABETICAL_DESC = new StoreSortMode() { + @Override + public StoreSection representative(StoreSection s) { + return s; + } + @Override public String getId() { return "alphabetical-desc"; @@ -23,8 +28,12 @@ public interface StoreSortMode { e -> e.getWrapper().nameProperty().getValue().toLowerCase(Locale.ROOT)); } }; - StoreSortMode ALPHABETICAL_ASC = new StoreSortMode() { + @Override + public StoreSection representative(StoreSection s) { + return s; + } + @Override public String getId() { return "alphabetical-asc"; @@ -37,8 +46,21 @@ public interface StoreSortMode { .reversed(); } }; - 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"; @@ -54,8 +76,21 @@ 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"; @@ -64,13 +99,15 @@ public interface StoreSortMode { @Override public Comparator comparator() { return Comparator.comparing(e -> { - return flatten(e) - .map(entry -> entry.getLastAccess()) - .max(Comparator.naturalOrder()) - .orElseThrow(); - }).reversed(); + return flatten(e) + .map(entry -> entry.getLastAccess()) + .max(Comparator.naturalOrder()) + .orElseThrow(); + }) + .reversed(); } }; + List ALL = List.of(ALPHABETICAL_DESC, ALPHABETICAL_ASC, DATE_DESC, DATE_ASC); static Stream flatten(StoreSection section) { return Stream.concat( @@ -78,14 +115,14 @@ public interface StoreSortMode { section.getAllChildren().stream().flatMap(section1 -> flatten(section1))); } - List ALL = List.of(ALPHABETICAL_DESC, ALPHABETICAL_ASC, DATE_DESC, DATE_ASC); - static Optional fromId(String id) { return ALL.stream() .filter(storeSortMode -> storeSortMode.getId().equals(id)) .findFirst(); } + StoreSection representative(StoreSection s); + String getId(); Comparator comparator(); 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 690d112d..8b6d019b 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; @@ -23,6 +24,26 @@ import java.util.stream.Collectors; public class StoreViewState { private static StoreViewState INSTANCE; + private final StringProperty filter = new SimpleStringProperty(); + + @Getter + private final ObservableList allEntries = + FXCollections.observableList(new CopyOnWriteArrayList<>()); + + @Getter + private final ObservableList categories = + FXCollections.observableList(new CopyOnWriteArrayList<>()); + + @Getter + private final Property activeCategory = new SimpleObjectProperty<>(); + + @Getter + private StoreSection currentTopLevelSection; + + private StoreViewState() { + initContent(); + addListeners(); + } public static void init() { if (INSTANCE != null) { @@ -52,27 +73,6 @@ public class StoreViewState { return INSTANCE; } - private final StringProperty filter = new SimpleStringProperty(); - - @Getter - private final ObservableList allEntries = - FXCollections.observableList(new CopyOnWriteArrayList<>()); - - @Getter - private final ObservableList categories = - FXCollections.observableList(new CopyOnWriteArrayList<>()); - - @Getter - private StoreSection currentTopLevelSection; - - @Getter - private final Property activeCategory = new SimpleObjectProperty<>(); - - private StoreViewState() { - initContent(); - addStorageListeners(); - } - private void updateContent() { categories.forEach(c -> c.update()); allEntries.forEach(e -> e.update()); @@ -112,12 +112,27 @@ 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 public void onStoreAdd(DataStoreEntry... entry) { - var l = Arrays.stream(entry).map(StoreEntryWrapper::new).peek(storeEntryWrapper -> storeEntryWrapper.update()).toList(); + var l = Arrays.stream(entry) + .map(StoreEntryWrapper::new) + .peek(storeEntryWrapper -> storeEntryWrapper.update()) + .toList(); Platform.runLater(() -> { // Don't update anything if we have already reset if (INSTANCE == null) { diff --git a/app/src/main/java/io/xpipe/app/core/App.java b/app/src/main/java/io/xpipe/app/core/App.java index cbc71101..49e419a4 100644 --- a/app/src/main/java/io/xpipe/app/core/App.java +++ b/app/src/main/java/io/xpipe/app/core/App.java @@ -1,6 +1,5 @@ package io.xpipe.app.core; -import io.xpipe.app.Main; import io.xpipe.app.comp.AppLayoutComp; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.issue.ErrorEvent; @@ -12,8 +11,8 @@ import javafx.application.Application; import javafx.beans.binding.Bindings; import javafx.stage.Stage; import lombok.Getter; +import lombok.SneakyThrows; -import javax.imageio.ImageIO; import java.awt.*; @Getter @@ -27,26 +26,13 @@ public class App extends Application { } @Override + @SneakyThrows public void start(Stage primaryStage) { TrackEvent.info("Application launched"); APP = this; stage = primaryStage; stage.opacityProperty().bind(AppPrefs.get().windowOpacity()); - // Set dock icon explicitly on mac - // This is necessary in case XPipe was started through a script as it will have no icon otherwise - if (OsType.getLocal().equals(OsType.MACOS) && AppProperties.get().isDeveloperMode() && AppLogs.get().isWriteToSysout()) { - try { - var iconUrl = Main.class.getResourceAsStream("resources/img/logo/logo_macos_128x128.png"); - if (iconUrl != null) { - var awtIcon = ImageIO.read(iconUrl); - Taskbar.getTaskbar().setIconImage(awtIcon); - } - } catch (Exception ex) { - ErrorEvent.fromThrowable(ex).omitted(true).build().handle(); - } - } - if (OsType.getLocal().equals(OsType.MACOS)) { Desktop.getDesktop().setPreferencesHandler(e -> { AppLayoutModel.get().selectSettings(); @@ -56,7 +42,8 @@ public class App extends Application { if (OsType.getLocal().equals(OsType.LINUX)) { try { Toolkit xToolkit = Toolkit.getDefaultToolkit(); - java.lang.reflect.Field awtAppClassNameField = xToolkit.getClass().getDeclaredField("awtAppClassName"); + java.lang.reflect.Field awtAppClassNameField = + xToolkit.getClass().getDeclaredField("awtAppClassName"); awtAppClassNameField.setAccessible(true); awtAppClassNameField.set(xToolkit, "XPipe"); } catch (Exception e) { @@ -103,10 +90,7 @@ public class App extends Application { public void focus() { PlatformThread.runLaterIfNeeded(() -> { - stage.setAlwaysOnTop(true); - stage.setAlwaysOnTop(false); stage.requestFocus(); }); } - } diff --git a/app/src/main/java/io/xpipe/app/core/AppBundledFonts.java b/app/src/main/java/io/xpipe/app/core/AppBundledFonts.java index 92c77e33..6f1381db 100644 --- a/app/src/main/java/io/xpipe/app/core/AppBundledFonts.java +++ b/app/src/main/java/io/xpipe/app/core/AppBundledFonts.java @@ -16,7 +16,8 @@ public class AppBundledFonts { return; } - System.setProperty("prism.fontdir", XPipeInstallation.getBundledFontsPath().toString()); + System.setProperty( + "prism.fontdir", XPipeInstallation.getBundledFontsPath().toString()); System.setProperty("prism.embeddedfonts", "true"); } diff --git a/app/src/main/java/io/xpipe/app/core/AppDebugModeNotice.java b/app/src/main/java/io/xpipe/app/core/AppDebugModeNotice.java index 239cf1a3..a005a317 100644 --- a/app/src/main/java/io/xpipe/app/core/AppDebugModeNotice.java +++ b/app/src/main/java/io/xpipe/app/core/AppDebugModeNotice.java @@ -11,8 +11,9 @@ public class AppDebugModeNotice { } var out = AppLogs.get().getOriginalSysOut(); - var msg = """ - + var msg = + """ + **************************************** * You are running XPipe in debug mode! * * The debug console output can contain * 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 db6d615a..a1b009b0 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; @@ -29,6 +28,7 @@ public class AppExtensionManager { private final List leafModuleLayers = new ArrayList<>(); private final List extensionBaseDirectories = new ArrayList<>(); private ModuleLayer baseLayer = ModuleLayer.boot(); + @Getter private ModuleLayer extendedLayer; @@ -52,11 +52,20 @@ public class AppExtensionManager { XPipeServiceProviders.load(INSTANCE.extendedLayer); MessageExchangeImpls.loadAll(); } catch (Throwable t) { - throw new ExtensionException("Service provider initialization failed. Is the installation data corrupt?", t); + throw new ExtensionException( + "Service provider initialization failed. Is the installation data corrupt?", t); } } } + public static void reset() { + INSTANCE = null; + } + + public static AppExtensionManager getInstance() { + return INSTANCE; + } + private void loadBaseExtension() { var baseModule = findAndParseExtension("base", ModuleLayer.boot()); if (baseModule.isEmpty()) { @@ -95,14 +104,6 @@ public class AppExtensionManager { extensionBaseDirectories.add(productionRoot); } - public static void reset() { - INSTANCE = null; - } - - public static AppExtensionManager getInstance() { - return INSTANCE; - } - public Set getContentModules() { return Stream.concat( Stream.of(ModuleLayer.boot().findModule("io.xpipe.app").orElseThrow()), @@ -110,87 +111,28 @@ 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) { - var scl = ClassLoader.getSystemClassLoader(); - var cfs = leafModuleLayers.stream().map(ModuleLayer::configuration).toList(); - var finder = ModuleFinder.ofSystem(); - var cf = Configuration.resolve(finder, cfs, finder, List.of()); - extendedLayer = ModuleLayer.defineModulesWithOneLoader(cf, leafModuleLayers, scl) - .layer(); - } else { - extendedLayer = baseLayer; - } - } - - 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(); - } + var scl = ClassLoader.getSystemClassLoader(); + var cfs = leafModuleLayers.stream().map(ModuleLayer::configuration).toList(); + var finder = ModuleFinder.ofSystem(); + var cf = Configuration.resolve(finder, cfs, finder, List.of()); + extendedLayer = ModuleLayer.defineModulesWithOneLoader(cf, leafModuleLayers, scl) + .layer(); } 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 +148,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 c9f8e835..dc8e1cfa 100644 --- a/app/src/main/java/io/xpipe/app/core/AppFileWatcher.java +++ b/app/src/main/java/io/xpipe/app/core/AppFileWatcher.java @@ -107,6 +107,7 @@ public class AppFileWatcher { private class WatchedDirectory { private final BiConsumer> listener; + @Getter private final Path baseDir; @@ -114,9 +115,7 @@ public class AppFileWatcher { this.baseDir = dir; this.listener = listener; createRecursiveWatchers(dir); - TrackEvent.withTrace("watcher", "Added watched directory") - .tag("location", dir) - .handle(); + TrackEvent.withTrace("Added watched directory").tag("location", dir).handle(); } private void createRecursiveWatchers(Path dir) { @@ -177,13 +176,12 @@ 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()) .handle(); listener.accept(file, ev.kind()); } - } } diff --git a/app/src/main/java/io/xpipe/app/core/AppFont.java b/app/src/main/java/io/xpipe/app/core/AppFont.java index f14aba45..2ad6e05b 100644 --- a/app/src/main/java/io/xpipe/app/core/AppFont.java +++ b/app/src/main/java/io/xpipe/app/core/AppFont.java @@ -54,7 +54,8 @@ public class AppFont { try (var in = Files.newInputStream(file)) { Font.loadFont(in, OsType.getLocal() == OsType.LINUX ? 11 : 12); } catch (Throwable t) { - // Font loading can fail in rare cases. This is however not important, so we can just ignore it + // Font loading can fail in rare cases. This is however not important, so we can just ignore + // it } return FileVisitResult.CONTINUE; } 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 6fe40ced..08be1218 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 a7b5599e..327042b3 100644 --- a/app/src/main/java/io/xpipe/app/core/AppI18n.java +++ b/app/src/main/java/io/xpipe/app/core/AppI18n.java @@ -37,10 +37,10 @@ import java.util.regex.Pattern; public class AppI18n { private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\w+?\\$"); + private static final AppI18n INSTANCE = new AppI18n(); private Map translations; private Map markdownDocumentations; private PrettyTime prettyTime; - private static final AppI18n INSTANCE = new AppI18n(); public static void init() { var i = INSTANCE; @@ -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(); }); @@ -98,8 +98,11 @@ public class AppI18n { return "null"; } - return getInstance().prettyTime.formatDuration( - getInstance().prettyTime.approximateDuration(Instant.now().plus(duration.getValue()))); + return getInstance() + .prettyTime + .formatDuration(getInstance() + .prettyTime + .approximateDuration(Instant.now().plus(duration.getValue()))); }, duration); } @@ -136,20 +139,6 @@ public class AppI18n { return s; } - private void clear() { - translations.clear(); - prettyTime = null; - } - - @SuppressWarnings("removal") - public static class CallingClass extends SecurityManager { - public static final CallingClass INSTANCE = new CallingClass(); - - public Class[] getCallingClasses() { - return getClassContext(); - } - } - @SneakyThrows private static String getCallerModuleName() { var callers = CallingClass.INSTANCE.getCallingClasses(); @@ -161,6 +150,7 @@ public class AppI18n { || caller.equals(FancyTooltipAugment.class) || caller.equals(PrefsChoiceValue.class) || caller.equals(Translatable.class) + || caller.equals(AppWindowHelper.class) || caller.equals(OptionsBuilder.class)) { continue; } @@ -170,6 +160,11 @@ public class AppI18n { return ""; } + private void clear() { + translations.clear(); + prettyTime = null; + } + public String getKey(String s) { var key = s; if (!s.contains(".")) { @@ -210,7 +205,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(); @@ -219,7 +214,8 @@ public class AppI18n { public String getMarkdownDocumentation(String name) { if (!markdownDocumentations.containsKey(name)) { - TrackEvent.withWarn("Markdown documentation for key " + name + " not found").handle(); + TrackEvent.withWarn("Markdown documentation for key " + name + " not found") + .handle(); } return markdownDocumentations.getOrDefault(name, ""); @@ -311,7 +307,16 @@ public class AppI18n { this.prettyTime = new PrettyTime( AppPrefs.get() != null - ? AppPrefs.get().language.getValue().getLocale() + ? AppPrefs.get().language().getValue().getLocale() : SupportedLocale.ENGLISH.getLocale()); } + + @SuppressWarnings("removal") + public static class CallingClass extends SecurityManager { + public static final CallingClass INSTANCE = new CallingClass(); + + public Class[] getCallingClasses() { + return getClassContext(); + } + } } 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 4175b2ac..8860ef33 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,8 @@ 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.fxcomps.util.PlatformThread; +import io.xpipe.app.prefs.AppPrefsComp; import io.xpipe.app.util.LicenseProvider; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; @@ -18,20 +19,26 @@ import lombok.extern.jackson.Jacksonized; import java.util.ArrayList; import java.util.List; -@Getter public class AppLayoutModel { - @Data - @Builder - @Jacksonized - public static class SavedState { - - double sidebarWidth; - double browserConnectionsWidth; - } - private static AppLayoutModel INSTANCE; + @Getter + private final SavedState savedState; + + @Getter + private final List entries; + + private final Property selected; + private final ObservableValue selectedWrapper; + + public AppLayoutModel(SavedState savedState) { + this.savedState = savedState; + this.entries = createEntryList(); + this.selected = new SimpleObjectProperty<>(entries.get(1)); + this.selectedWrapper = PlatformThread.sync(selected); + } + public static AppLayoutModel get() { return INSTANCE; } @@ -46,19 +53,16 @@ public class AppLayoutModel { INSTANCE = null; } - @Getter - private final SavedState savedState; - private final List entries; - private final Property selected; + public Property getSelectedInternal() { + return selected; + } - public AppLayoutModel(SavedState savedState) { - this.savedState = savedState; - this.entries = createEntryList(); - this.selected = new SimpleObjectProperty<>(entries.get(1)); + public ObservableValue getSelected() { + return selectedWrapper; } public void selectBrowser() { - selected.setValue(entries.get(0)); + selected.setValue(entries.getFirst()); } public void selectSettings() { @@ -75,17 +79,14 @@ public class AppLayoutModel { private List createEntryList() { var l = new ArrayList<>(List.of( - new Entry( - AppI18n.observable("browser"), "mdi2f-file-cabinet", new BrowserComp(BrowserModel.DEFAULT)), + new Entry(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)))); + new Entry(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()) if (AppProperties.get().isDeveloperMode() && !AppProperties.get().isImage()) { - l.add(new Entry( - AppI18n.observable("developer"), "mdi2b-book-open-variant", new DeveloperTabComp())); + l.add(new Entry(AppI18n.observable("developer"), "mdi2b-book-open-variant", new DeveloperTabComp())); } l.add(new Entry( @@ -96,5 +97,14 @@ public class AppLayoutModel { return l; } + @Data + @Builder + @Jacksonized + public static class SavedState { + + double sidebarWidth; + double browserConnectionsWidth; + } + public record Entry(ObservableValue name, String icon, Comp comp) {} } 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 a4ba4d8b..0973f32b 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,23 +31,26 @@ 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()); private static AppLogs INSTANCE; + @Getter private final PrintStream originalSysOut; + @Getter private final PrintStream originalSysErr; + private final Path logDir; @Getter @@ -60,16 +62,15 @@ 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 +97,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 +132,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() { @@ -149,45 +175,19 @@ public class AppLogs { return INSTANCE; } - private void close() { - outStream.close(); - categoryWriters.forEach((k, s) -> { - s.close(); - }); - } - - private String getCategory(TrackEvent event) { - if (event.getCategory() != null) { - return event.getCategory(); + private static String determineLogLevel() { + if (System.getProperty(LOG_LEVEL_PROP) != null) { + String p = System.getProperty(LOG_LEVEL_PROP); + return LOG_LEVELS.contains(p) ? p : "trace"; } - return "misc"; + return DEFAULT_LOG_LEVEL; } - 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 void close() { + if (outFileStream != null) { + outFileStream.close(); + } } private boolean shouldDebugPlatform() { @@ -210,12 +210,7 @@ public class AppLogs { return; } - TrackEvent.builder() - .type("info") - .category("sysout") - .message(line) - .build() - .handle(); + TrackEvent.builder().type("info").message(line).build().handle(); baos.reset(); } else { baos.write(b); @@ -245,15 +240,6 @@ 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 DEFAULT_LOG_LEVEL; - } - public void logException(String description, Throwable e) { var deob = Deobfuscator.deobfuscateToString(e); var event = TrackEvent.builder() @@ -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; } @@ -339,7 +317,7 @@ public class AppLogs { normalizedName = name; } - return loggers.computeIfAbsent(normalizedName, Slf4jLogger::new); + return loggers.computeIfAbsent(normalizedName, s -> new Slf4jLogger()); } }; @@ -369,12 +347,6 @@ public class AppLogs { public static final class Slf4jLogger extends AbstractLogger { - private final String name; - - public Slf4jLogger(String name) { - this.name = name; - } - @Override protected String getFullyQualifiedCallerName() { return "logger"; @@ -390,7 +362,6 @@ public class AppLogs { } } TrackEvent.builder() - .category(name) .type(level.toString().toLowerCase()) .message(msg) .build() @@ -399,62 +370,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 6ce9273c..5c4c60da 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 d498da87..e5aac67d 100644 --- a/app/src/main/java/io/xpipe/app/core/AppProperties.java +++ b/app/src/main/java/io/xpipe/app/core/AppProperties.java @@ -19,15 +19,20 @@ public class AppProperties { private static final String EXTENSION_PATHS_PROP = "io.xpipe.app.extensions"; private static AppProperties INSTANCE; boolean fullVersion; + @Getter String version; + @Getter String build; + UUID buildUuid; String sentryUrl; String arch; + @Getter boolean image; + boolean staging; boolean useVirtualThreads; boolean debugThreads; @@ -101,6 +106,10 @@ public class AppProperties { return INSTANCE; } + public boolean isDevelopmentEnvironment() { + return !AppProperties.get().isImage() && AppProperties.get().isDeveloperMode(); + } + public boolean isDeveloperMode() { if (AppPrefs.get() == null) { return false; @@ -108,5 +117,4 @@ public class AppProperties { return AppPrefs.get().developerMode().getValue(); } - } 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 6088d530..29f127a2 100644 --- a/app/src/main/java/io/xpipe/app/core/AppSocketServer.java +++ b/app/src/main/java/io/xpipe/app/core/AppSocketServer.java @@ -57,7 +57,10 @@ public class AppSocketServer { .handle(); } catch (Exception ex) { // Not terminal! - ErrorEvent.fromThrowable(ex).description("Unable to start local socket server on port " + port).build().handle(); + ErrorEvent.fromThrowable(ex) + .description("Unable to start local socket server on port " + port) + .build() + .handle(); } } @@ -112,7 +115,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 +124,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 +148,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 +173,6 @@ public class AppSocketServer { } TrackEvent.builder() - .category("beacon") .type("trace") .message("Socket connection #" + id + " performed exchange " + req.getClass().getSimpleName()) @@ -187,7 +189,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 +199,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 +212,29 @@ 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); + var toReport = se.getCause() != null ? se.getCause() : se; + ErrorEvent.fromThrowable(toReport).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,16 +244,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"); + TrackEvent.builder().type("trace").message("Socket connection #" + id + " finished unsuccessfully"); } private void performExchangesAsync(Socket clientSocket) { @@ -296,7 +294,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 +334,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/AppState.java b/app/src/main/java/io/xpipe/app/core/AppState.java index bd479ab5..9c830f8a 100644 --- a/app/src/main/java/io/xpipe/app/core/AppState.java +++ b/app/src/main/java/io/xpipe/app/core/AppState.java @@ -15,8 +15,9 @@ public class AppState { boolean initialLaunch; @NonFinal - @Setter + @Setter String userName; + @NonFinal @Setter String userEmail; 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 44301de2..30f69d9f 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,17 +48,19 @@ 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) { try { var bytes = Files.readAllBytes(file); if (file.getFileName().toString().endsWith(".bss")) { - var s = "data:application/octet-stream;base64," + Base64.getEncoder().encodeToString(bytes); + var s = "data:application/octet-stream;base64," + + Base64.getEncoder().encodeToString(bytes); STYLESHEET_CONTENTS.put(file, s); } else if (file.getFileName().toString().endsWith(".css")) { - var s = "data:text/css;base64," + Base64.getEncoder().encodeToString(bytes); + var s = "data:text/css;base64," + + Base64.getEncoder().encodeToString(bytes); STYLESHEET_CONTENTS.put(file, s); } } catch (IOException ex) { @@ -93,7 +95,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 dc8e9726..729c6ddb 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,23 @@ 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 +105,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 +184,8 @@ public class AppTheme { } @Override - public String toTranslatedString() { - return name; + public ObservableValue toTranslatedString() { + return new SimpleStringProperty(name); } } @@ -211,6 +206,12 @@ public class AppTheme { // Also include your custom theme here public static final List ALL = List.of(PRIMER_LIGHT, PRIMER_DARK, NORD_LIGHT, NORD_DARK, CUPERTINO_LIGHT, CUPERTINO_DARK, DRACULA); + protected final String id; + + @Getter + protected final String cssId; + + protected final atlantafx.base.theme.Theme theme; static Theme getDefaultLightTheme() { return switch (OsType.getLocal()) { @@ -228,13 +229,6 @@ public class AppTheme { }; } - protected final String id; - - @Getter - protected final String cssId; - - protected final atlantafx.base.theme.Theme theme; - public boolean isDark() { return theme.isDarkMode(); } @@ -244,8 +238,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/AppTray.java b/app/src/main/java/io/xpipe/app/core/AppTray.java index 82d0cbd3..ac117a4d 100644 --- a/app/src/main/java/io/xpipe/app/core/AppTray.java +++ b/app/src/main/java/io/xpipe/app/core/AppTray.java @@ -14,6 +14,7 @@ public class AppTray { private static AppTray INSTANCE; private final AppTrayIcon icon; + @Getter private final ErrorHandler errorHandler; diff --git a/app/src/main/java/io/xpipe/app/core/AppTrayIcon.java b/app/src/main/java/io/xpipe/app/core/AppTrayIcon.java index 87548b3c..e447772b 100644 --- a/app/src/main/java/io/xpipe/app/core/AppTrayIcon.java +++ b/app/src/main/java/io/xpipe/app/core/AppTrayIcon.java @@ -14,21 +14,23 @@ public class AppTrayIcon { private final SystemTray tray; private final TrayIcon trayIcon; - private final PopupMenu popupMenu = new PopupMenu(); public AppTrayIcon() { ensureSystemTraySupported(); tray = SystemTray.getSystemTray(); - var image = switch (OsType.getLocal()) { - case OsType.Windows windows -> "img/logo/logo_16x16.png"; - case OsType.Linux linux -> "img/logo/logo_24x24.png"; - case OsType.MacOs macOs -> "img/logo/logo_macos_tray_24x24.png"; - }; + var image = + switch (OsType.getLocal()) { + case OsType.Windows windows -> "img/logo/logo_16x16.png"; + case OsType.Linux linux -> "img/logo/logo_24x24.png"; + case OsType.MacOs macOs -> "img/logo/logo_macos_tray_24x24.png"; + }; var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, image).orElseThrow(); - this.trayIcon = new TrayIcon(loadImageFromURL(url), App.getApp().getStage().getTitle(), popupMenu); + PopupMenu popupMenu = new PopupMenu(); + this.trayIcon = + new TrayIcon(loadImageFromURL(url), App.getApp().getStage().getTitle(), popupMenu); this.trayIcon.setToolTip("XPipe"); this.trayIcon.setImageAutoSize(true); @@ -58,6 +60,19 @@ public class AppTrayIcon { }); } + private static Image loadImageFromURL(URL iconImagePath) { + try { + return ImageIO.read(iconImagePath); + } catch (IOException e) { + ErrorEvent.fromThrowable(e).handle(); + return AppImages.toAwtImage(AppImages.DEFAULT_IMAGE); + } + } + + public static boolean isSupported() { + return Desktop.isDesktopSupported() && SystemTray.isSupported(); + } + public final TrayIcon getAwtTrayIcon() { return trayIcon; } @@ -65,17 +80,7 @@ public class AppTrayIcon { private void ensureSystemTraySupported() { if (!SystemTray.isSupported()) { throw new UnsupportedOperationException( - "SystemTray icons are not " - + "supported by the current desktop environment."); - } - } - - private static Image loadImageFromURL(URL iconImagePath) { - try { - return ImageIO.read(iconImagePath); - } catch (IOException e) { - ErrorEvent.fromThrowable(e).handle(); - return AppImages.toAwtImage(AppImages.DEFAULT_IMAGE); + "SystemTray icons are not " + "supported by the current desktop environment."); } } @@ -129,11 +134,9 @@ public class AppTrayIcon { public void showInfoMessage(String title, String message) { if (OsType.getLocal().equals(OsType.MACOS)) { - showMacAlert(title, message,"Information"); + showMacAlert(title, message, "Information"); } else { - EventQueue.invokeLater(() -> - this.trayIcon.displayMessage( - title, message, TrayIcon.MessageType.INFO)); + EventQueue.invokeLater(() -> this.trayIcon.displayMessage(title, message, TrayIcon.MessageType.INFO)); } } @@ -143,11 +146,9 @@ public class AppTrayIcon { public void showWarningMessage(String title, String message) { if (OsType.getLocal().equals(OsType.MACOS)) { - showMacAlert(title, message,"Warning"); + showMacAlert(title, message, "Warning"); } else { - EventQueue.invokeLater(() -> - this.trayIcon.displayMessage( - title, message, TrayIcon.MessageType.WARNING)); + EventQueue.invokeLater(() -> this.trayIcon.displayMessage(title, message, TrayIcon.MessageType.WARNING)); } } @@ -157,11 +158,9 @@ public class AppTrayIcon { public void showErrorMessage(String title, String message) { if (OsType.getLocal().equals(OsType.MACOS)) { - showMacAlert(title, message,"Error"); + showMacAlert(title, message, "Error"); } else { - EventQueue.invokeLater(() -> - this.trayIcon.displayMessage( - title, message, TrayIcon.MessageType.ERROR)); + EventQueue.invokeLater(() -> this.trayIcon.displayMessage(title, message, TrayIcon.MessageType.ERROR)); } } @@ -171,11 +170,9 @@ public class AppTrayIcon { public void showMessage(String title, String message) { if (OsType.getLocal().equals(OsType.MACOS)) { - showMacAlert(title, message,"Message"); + showMacAlert(title, message, "Message"); } else { - EventQueue.invokeLater(() -> - this.trayIcon.displayMessage( - title, message, TrayIcon.MessageType.NONE)); + EventQueue.invokeLater(() -> this.trayIcon.displayMessage(title, message, TrayIcon.MessageType.NONE)); } } @@ -183,26 +180,14 @@ public class AppTrayIcon { this.showMessage(null, message); } - public static boolean isSupported() { - return Desktop.isDesktopSupported() && SystemTray.isSupported(); - } - private void showMacAlert(String subTitle, String message, String title) { String execute = String.format( - "display notification \"%s\"" - + " with title \"%s\"" - + " subtitle \"%s\"", - message != null ? message : "", - title != null ? title : "", - subTitle != null ? subTitle : "" - ); + "display notification \"%s\"" + " with title \"%s\"" + " subtitle \"%s\"", + message != null ? message : "", title != null ? title : "", subTitle != null ? subTitle : ""); try { - Runtime.getRuntime() - .exec(new String[] { "osascript", "-e", execute }); + Runtime.getRuntime().exec(new String[] {"osascript", "-e", execute}); } catch (IOException e) { - throw new UnsupportedOperationException( - "Cannot run osascript with given parameters."); + throw new UnsupportedOperationException("Cannot run osascript with given parameters."); } } } - 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 da1765f0..15d961a7 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? @@ -100,8 +109,7 @@ public class AppWindowHelper { childStage.setY(stage.getY() + stage.getHeight() / 2 - childStage.getHeight() / 2); } - public static void showAlert( - Consumer c, Consumer> bt) { + public static void showAlert(Consumer c, Consumer> bt) { ThreadHelper.runAsync(() -> { var r = showBlockingAlert(c); if (bt != null) { @@ -110,6 +118,36 @@ 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(String title, String header, String content) { + return AppWindowHelper.showBlockingAlert(alert -> { + alert.titleProperty().bind(AppI18n.observable(title)); + alert.headerTextProperty().bind(AppI18n.observable(header)); + setContent(alert, AppI18n.get(content)); + alert.setAlertType(Alert.AlertType.CONFIRMATION); + }) + .map(b -> b.getButtonData().isDefaultButton()) + .orElse(false); + } + + 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(); @@ -224,7 +262,6 @@ public class AppWindowHelper { if (event.getCode().equals(KeyCode.W) && event.isShortcutDown()) { stage.close(); event.consume(); - return; } } }); @@ -236,7 +273,11 @@ public class AppWindowHelper { } var allScreenBounds = computeWindowScreenBounds(stage); - if (!areNumbersValid(allScreenBounds.getMinX(), allScreenBounds.getMinY(), allScreenBounds.getMaxX(), allScreenBounds.getMaxY())) { + if (!areNumbersValid( + allScreenBounds.getMinX(), + allScreenBounds.getMinY(), + allScreenBounds.getMaxX(), + allScreenBounds.getMaxY())) { return Optional.empty(); } @@ -287,43 +328,46 @@ public class AppWindowHelper { private static List getWindowScreens(Stage stage) { if (!areNumbersValid(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight())) { - return stage.getOwner() != null && stage.getOwner() instanceof Stage ownerStage ? getWindowScreens(ownerStage) : List.of(Screen.getPrimary()); + return stage.getOwner() != null && stage.getOwner() instanceof Stage ownerStage + ? getWindowScreens(ownerStage) + : List.of(Screen.getPrimary()); } - return Screen.getScreensForRectangle(new Rectangle2D(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight())); + return Screen.getScreensForRectangle( + new Rectangle2D(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight())); } private static Rectangle2D computeWindowScreenBounds(Stage stage) { - double minX = Double.POSITIVE_INFINITY ; - double minY = Double.POSITIVE_INFINITY ; - double maxX = Double.NEGATIVE_INFINITY ; - double maxY = Double.NEGATIVE_INFINITY ; + double minX = Double.POSITIVE_INFINITY; + double minY = Double.POSITIVE_INFINITY; + double maxX = Double.NEGATIVE_INFINITY; + double maxY = Double.NEGATIVE_INFINITY; for (Screen screen : getWindowScreens(stage)) { Rectangle2D screenBounds = screen.getBounds(); if (screenBounds.getMinX() < minX) { minX = screenBounds.getMinX(); } if (screenBounds.getMinY() < minY) { - minY = screenBounds.getMinY() ; + minY = screenBounds.getMinY(); } if (screenBounds.getMaxX() > maxX) { maxX = screenBounds.getMaxX(); } if (screenBounds.getMaxY() > maxY) { - maxY = screenBounds.getMaxY() ; + maxY = screenBounds.getMaxY(); } } // Taskbar adjustment maxY -= 50; - var w = maxX-minX; - var h = maxY-minY; + var w = maxX - minX; + var h = maxY - minY; // This should not happen but on weird Linux systems nothing is impossible if (w < 0 || h < 0) { - return new Rectangle2D(0,0,800, 600); + return new Rectangle2D(0, 0, 800, 600); } - + return new Rectangle2D(minX, minY, w, h); } diff --git a/app/src/main/java/io/xpipe/app/core/check/AppAvCheck.java b/app/src/main/java/io/xpipe/app/core/check/AppAvCheck.java index 2d9d2512..b90b314d 100644 --- a/app/src/main/java/io/xpipe/app/core/check/AppAvCheck.java +++ b/app/src/main/java/io/xpipe/app/core/check/AppAvCheck.java @@ -17,54 +17,6 @@ import java.util.Optional; public class AppAvCheck { - @Getter - public static enum AvType { - - BITDEFENDER("Bitdefender") { - @Override - public String getDescription() { - return "Bitdefender sometimes isolates XPipe and some shell programs, effectively making it unusable."; - } - - @Override - public boolean isActive() { - return WindowsRegistry.exists(WindowsRegistry.HKEY_LOCAL_MACHINE,"SOFTWARE\\Bitdefender", "InstallDir"); - } - }, - MALWAREBYTES("Malwarebytes") { - @Override - public String getDescription() { - return "The free Malwarebytes version performs less invasive scans, so it shouldn't be a problem. If you are running the paid Malwarebytes Pro version, you will have access to the `Exploit Protection` under the `Real-time Protection` mode. When this setting is active, any shell access is slowed down, resulting in XPipe becoming very slow."; - } - - @Override - public boolean isActive() { - return WindowsRegistry.exists(WindowsRegistry.HKEY_LOCAL_MACHINE,"SOFTWARE\\Malwarebytes", "id"); - } - }, - MCAFEE("McAfee") { - @Override - public String getDescription() { - return "McAfee slows down XPipe considerably. It also sometimes preemptively disables some Win32 commands that XPipe depends on, leading to errors."; - } - - @Override - public boolean isActive() { - return WindowsRegistry.exists(WindowsRegistry.HKEY_LOCAL_MACHINE,"SOFTWARE\\McAfee", "mi"); - } - }; - - private final String name; - - AvType(String name) { - this.name = name; - } - - public abstract String getDescription(); - - public abstract boolean isActive(); - } - private static Optional detect() { for (AvType value : AvType.values()) { if (value.isActive()) { @@ -93,21 +45,75 @@ public class AppAvCheck { alert.setTitle(AppI18n.get("antivirusNoticeTitle")); alert.setAlertType(Alert.AlertType.NONE); - AppResources.with( - AppResources.XPIPE_MODULE, - "misc/antivirus.md", - file -> { - var markdown = new MarkdownComp(Files.readString(file), s -> { + AppResources.with(AppResources.XPIPE_MODULE, "misc/antivirus.md", file -> { + var markdown = new MarkdownComp(Files.readString(file), s -> { var t = found.get(); - return s.formatted(t.getName(), t.getName(), t.getDescription(), AppProperties.get().getVersion(), AppProperties.get().getVersion(), t.getName()); - }).prefWidth(550).prefHeight(600).createRegion(); - alert.getDialogPane().setContent(markdown); - alert.getDialogPane().setPadding(new Insets(15)); - }); + return s.formatted( + t.getName(), + t.getName(), + t.getDescription(), + AppProperties.get().getVersion(), + AppProperties.get().getVersion(), + t.getName()); + }) + .prefWidth(550) + .prefHeight(600) + .createRegion(); + alert.getDialogPane().setContent(markdown); + alert.getDialogPane().setPadding(new Insets(15)); + }); alert.getButtonTypes().add(new ButtonType(AppI18n.get("gotIt"), ButtonBar.ButtonData.OK_DONE)); }); a.filter(b -> b.getButtonData().isDefaultButton()) .ifPresentOrElse(buttonType -> {}, () -> OperationMode.halt(1)); } + + @Getter + public enum AvType { + BITDEFENDER("Bitdefender") { + @Override + public String getDescription() { + return "Bitdefender sometimes isolates XPipe and some shell programs, effectively making it unusable."; + } + + @Override + public boolean isActive() { + return WindowsRegistry.exists( + WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\Bitdefender", "InstallDir"); + } + }, + MALWAREBYTES("Malwarebytes") { + @Override + public String getDescription() { + return "The free Malwarebytes version performs less invasive scans, so it shouldn't be a problem. If you are running the paid Malwarebytes Pro version, you will have access to the `Exploit Protection` under the `Real-time Protection` mode. When this setting is active, any shell access is slowed down, resulting in XPipe becoming very slow."; + } + + @Override + public boolean isActive() { + return WindowsRegistry.exists(WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\Malwarebytes", "id"); + } + }, + MCAFEE("McAfee") { + @Override + public String getDescription() { + return "McAfee slows down XPipe considerably. It also sometimes preemptively disables some Win32 commands that XPipe depends on, leading to errors."; + } + + @Override + public boolean isActive() { + return WindowsRegistry.exists(WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\McAfee", "mi"); + } + }; + + private final String name; + + AvType(String name) { + this.name = name; + } + + public abstract String getDescription(); + + public abstract boolean isActive(); + } } 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 00000000..c36be1f5 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/core/check/AppCertutilCheck.java @@ -0,0 +1,36 @@ +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") + "\\System32\\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 ce5283b1..2ae0086a 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; @@ -12,19 +12,25 @@ public class AppShellCheck { public static void check() { var err = selfTestErrorCheck(); if (err.isPresent()) { - var msg = """ + var msg = + """ Shell self-test failed for %s: %s - + This indicates that something is seriously wrong and certain shell functionality will not work as expected. - + The most likely causes are: - On Windows, an AntiVirus program might block required programs and commands - The system shell is restricted or blocked - 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/check/AppTempCheck.java b/app/src/main/java/io/xpipe/app/core/check/AppTempCheck.java index d0b7af27..33c3d455 100644 --- a/app/src/main/java/io/xpipe/app/core/check/AppTempCheck.java +++ b/app/src/main/java/io/xpipe/app/core/check/AppTempCheck.java @@ -18,8 +18,8 @@ public class AppTempCheck { } if (dir == null || !Files.exists(dir) || !Files.isDirectory(dir)) { - ErrorEvent.fromThrowable( - new IOException("Specified temporary directory " + tmpdir + ", set via the environment variable %TEMP% is invalid.")) + ErrorEvent.fromThrowable(new IOException("Specified temporary directory " + tmpdir + + ", set via the environment variable %TEMP% is invalid.")) .term() .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 704cca61..fd0bb139 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,18 @@ 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.storage.GitStorageHandler; 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.UnlockAlert; import io.xpipe.core.util.JacksonMapper; public class BaseMode extends OperationMode { @@ -39,29 +41,29 @@ 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.initLocal(); + AppCertutilCheck.check(); AppAvCheck.check(); LocalShell.init(); - AppShellCheck.check(); XPipeDistributionType.init(); - AppPrefs.init(); - AppCharsets.init(); - AppCharsetter.init(); + AppShellCheck.check(); + AppPrefs.setDefaults(); + // Initialize socket server as we should be prepared for git askpass commands AppSocketServer.init(); + GitStorageHandler.getInstance().init(); + GitStorageHandler.getInstance().setupRepositoryAndPull(); + AppPrefs.initSharedRemote(); + 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; } @@ -70,7 +72,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 +82,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 e4fa7978..ea144e23 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 { @@ -17,14 +16,23 @@ public class GuiMode extends PlatformMode { return "gui"; } + @Override + public void onSwitchFrom() { + PlatformThread.runLaterIfNeededBlocking(() -> { + TrackEvent.info("Closing windows"); + Stage.getWindows().stream().toList().forEach(w -> { + w.hide(); + }); + }); + } + @Override 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,18 +43,8 @@ public class GuiMode extends PlatformMode { } AppMainWindow.getInstance().show(); }); - TrackEvent.info("mode", "Window setup complete"); + TrackEvent.info("Window setup complete"); UpdateChangelogAlert.showIfNeeded(); } - - @Override - public void onSwitchFrom() { - PlatformThread.runLaterIfNeededBlocking(() -> { - TrackEvent.info("mode", "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 7ce66409..50abd66e 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 @@ -1,22 +1,29 @@ package io.xpipe.app.core.mode; +import io.xpipe.app.Main; import io.xpipe.app.core.*; import io.xpipe.app.core.check.AppTempCheck; import io.xpipe.app.core.check.AppUserDirectoryCheck; import io.xpipe.app.ext.DataStoreProviders; import io.xpipe.app.issue.*; import io.xpipe.app.launcher.LauncherCommand; +import io.xpipe.app.launcher.LauncherInput; import io.xpipe.app.util.LocalShell; import io.xpipe.app.util.PlatformState; import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.XPipeSession; +import io.xpipe.core.process.OsType; 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; +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.desktop.AppReopenedEvent; +import java.awt.desktop.AppReopenedListener; +import java.awt.desktop.SystemEventListener; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; @@ -29,12 +36,16 @@ public abstract class OperationMode { public static final OperationMode GUI = new GuiMode(); private static final Pattern PROPERTY_PATTERN = Pattern.compile("^-[DP](.+)=(.+)$"); private static final List ALL = List.of(BACKGROUND, TRAY, GUI); + @Getter private static boolean inStartup; + @Getter private static boolean inShutdown; + @Getter private static boolean inShutdownHook; + private static OperationMode CURRENT = null; public static OperationMode map(XPipeDaemonMode mode) { @@ -86,14 +97,14 @@ public abstract class OperationMode { // Handle uncaught exceptions Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> { - ErrorEvent.fromThrowable(ex).build().handle(); + ErrorEvent.fromThrowable(ex).unhandled(true).build().handle(); }); // if (true) { // throw new OutOfMemoryError(); // } - TrackEvent.info("mode", "Initial setup"); + TrackEvent.info("Initial setup"); AppProperties.init(); AppState.init(); XPipeSession.init(AppProperties.get().getBuildUuid()); @@ -104,8 +115,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(); } @@ -121,24 +131,66 @@ public abstract class OperationMode { } public static void postInit(String[] args) { - DataStoreProviders.postInit(AppExtensionManager.getInstance().getExtendedLayer()); + try { + // This will initialize the toolkit on macos and create the dock icon + // macOS it does not like applications that run fully in the background, so do it always + if (OsType.getLocal().equals(OsType.MACOS)) { + // URL open operations have to be handled in a special way on macOS! + Desktop.getDesktop().setOpenURIHandler(e -> { + LauncherInput.handle(List.of(e.getURI().toString())); + }); + + // Do it this way to prevent IDE inspections from complaining + var c = Class.forName( + ModuleLayer.boot().findModule("java.desktop").orElseThrow(), "com.apple.eawt.Application"); + var m = c.getDeclaredMethod("addAppEventListener", SystemEventListener.class); + m.invoke(c.getMethod("getApplication").invoke(null), new AppReopenedListener() { + @Override + public void appReopened(AppReopenedEvent e) { + OperationMode.switchToAsync(OperationMode.GUI); + } + }); + + // Set dock icon explicitly on mac + // This is necessary in case XPipe was started through a script as it will have no icon otherwise + if (AppProperties.get().isDeveloperMode() && AppLogs.get().isWriteToSysout()) { + try { + var iconUrl = Main.class.getResourceAsStream("resources/img/logo/logo_macos_128x128.png"); + if (iconUrl != null) { + var awtIcon = ImageIO.read(iconUrl); + Taskbar.getTaskbar().setIconImage(awtIcon); + } + } catch (Exception ex) { + ErrorEvent.fromThrowable(ex).omitted(true).build().handle(); + } + } + } + + DataStoreProviders.postInit(AppExtensionManager.getInstance().getExtendedLayer()); + } catch (Throwable ex) { + ErrorEvent.fromThrowable(ex).term().handle(); + } } public static void switchToAsync(OperationMode newMode) { ThreadHelper.createPlatformThread("mode switcher", false, () -> { - switchToSyncIfPossible(newMode); - }).start(); + switchToSyncIfPossible(newMode); + }) + .start(); } public static void switchToSyncOrThrow(OperationMode newMode) throws Throwable { TrackEvent.info("Attempting to switch mode to " + newMode.getId()); if (!newMode.isSupported()) { - throw PlatformState.getLastError() != null ? PlatformState.getLastError() : new IllegalStateException("Unsupported operation mode: " + newMode.getId()); + throw PlatformState.getLastError() != null + ? PlatformState.getLastError() + : new IllegalStateException("Unsupported operation mode: " + newMode.getId()); } set(newMode); } + public static boolean switchToSyncIfPossible(OperationMode newMode) { TrackEvent.info("Attempting to switch mode to " + newMode.getId()); @@ -158,7 +210,6 @@ public abstract class OperationMode { return true; } - public static void switchUp(OperationMode newMode) { if (newMode == BACKGROUND) { return; @@ -187,7 +238,8 @@ public abstract class OperationMode { public static void restart() { OperationMode.executeAfterShutdown(() -> { - var exec = XPipeInstallation.createExternalAsyncLaunchCommand(XPipeInstallation.getLocalDefaultInstallationBasePath(), XPipeDaemonMode.GUI, ""); + var exec = XPipeInstallation.createExternalAsyncLaunchCommand( + XPipeInstallation.getLocalDefaultInstallationBasePath(), XPipeDaemonMode.GUI, ""); LocalShell.getShell().executeSimpleCommand(exec); }); } @@ -211,11 +263,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 +297,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 ..."); @@ -273,20 +320,20 @@ public abstract class OperationMode { OperationMode.halt(hasError ? 1 : 0); } -// public static synchronized void reload() { -// ThreadHelper.create("reloader", false, () -> { -// try { -// switchTo(BACKGROUND); -// CURRENT.finalTeardown(); -// CURRENT.onSwitchTo(); -// switchTo(GUI); -// } catch (Throwable t) { -// ErrorEvent.fromThrowable(t).build().handle(); -// OperationMode.halt(1); -// } -// }) -// .start(); -// } + // public static synchronized void reload() { + // ThreadHelper.create("reloader", false, () -> { + // try { + // switchTo(BACKGROUND); + // CURRENT.finalTeardown(); + // CURRENT.onSwitchTo(); + // switchTo(GUI); + // } catch (Throwable t) { + // ErrorEvent.fromThrowable(t).build().handle(); + // OperationMode.halt(1); + // } + // }) + // .start(); + // } private static synchronized void set(OperationMode newMode) { if (CURRENT == null && newMode == null) { 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 12b1db6e..160ccb08 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 6358b9b7..83bed40c 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 @@ -3,6 +3,7 @@ package io.xpipe.app.core.mode; import io.xpipe.app.core.AppTray; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.issue.*; +import io.xpipe.core.process.OsType; import java.awt.*; @@ -10,7 +11,24 @@ public class TrayMode extends PlatformMode { @Override public boolean isSupported() { - return super.isSupported() && Desktop.isDesktopSupported() && SystemTray.isSupported(); + return !OsType.getLocal().equals(OsType.MACOS) + && super.isSupported() + && Desktop.isDesktopSupported() + && SystemTray.isSupported(); + } + + @Override + public void onSwitchTo() throws Throwable { + super.onSwitchTo(); + PlatformThread.runLaterIfNeededBlocking(() -> { + if (AppTray.get() == null) { + TrackEvent.info("Initializing tray"); + AppTray.init(); + } + + AppTray.get().show(); + TrackEvent.info("Finished tray initialization"); + }); } @Override @@ -18,24 +36,10 @@ public class TrayMode extends PlatformMode { return "tray"; } - @Override - public void onSwitchTo() throws Throwable { - super.onSwitchTo(); - PlatformThread.runLaterIfNeededBlocking(() -> { - if (AppTray.get() == null) { - TrackEvent.info("mode", "Initializing tray"); - AppTray.init(); - } - - AppTray.get().show(); - TrackEvent.info("mode", "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 47b0c612..c91f47c6 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,20 @@ 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 a65be5bc..b6d46c20 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 f3886a5a..74082a05 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,8 @@ 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(); } @@ -26,9 +27,8 @@ public class LaunchExchangeImpl extends LaunchExchange private List split(String command) { var split = Arrays.stream(command.split(" ", 3)).collect(Collectors.toList()); var s = split.get(2); - if ((s.startsWith("\"") && s.endsWith("\"")) - || (s.startsWith("'") && s.endsWith("'"))) { - split.set(2,s.substring(1, s.length() - 1)); + if ((s.startsWith("\"") && s.endsWith("\"")) || (s.startsWith("'") && s.endsWith("'"))) { + split.set(2, s.substring(1, s.length() - 1)); } return split; } diff --git a/app/src/main/java/io/xpipe/app/exchange/MessageExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/MessageExchangeImpl.java index 94c3cb84..4bf19c23 100644 --- a/app/src/main/java/io/xpipe/app/exchange/MessageExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/exchange/MessageExchangeImpl.java @@ -34,8 +34,8 @@ public interface MessageExchangeImpl { + + @Override + public Response handleRequest(BeaconHandler handler, Request msg) throws 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 00000000..bac98a9e --- /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 1b91e049..d08a88c6 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 edd3187d..79ddef7f 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/exchange/cli/StoreAddExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/cli/StoreAddExchangeImpl.java index 9112598f..3ade3f85 100644 --- a/app/src/main/java/io/xpipe/app/exchange/cli/StoreAddExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/exchange/cli/StoreAddExchangeImpl.java @@ -90,9 +90,7 @@ public class StoreAddExchangeImpl extends StoreAddExchange try { } catch (Exception ignored) { } - if (d != null) { - d = d.indent(2); - } + d = d.indent(2); return "Successfully created data store " + name.get() + ":\n" + d; }); diff --git a/app/src/main/java/io/xpipe/app/exchange/cli/StoreProviderListExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/cli/StoreProviderListExchangeImpl.java index 5203edd9..50833abe 100644 --- a/app/src/main/java/io/xpipe/app/exchange/cli/StoreProviderListExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/exchange/cli/StoreProviderListExchangeImpl.java @@ -19,8 +19,7 @@ public class StoreProviderListExchangeImpl extends StoreProviderListExchange var all = DataStoreProviders.getAll(); var map = Arrays.stream(categories) .collect(Collectors.toMap(category -> getName(category), category -> all.stream() - .filter(dataStoreProvider -> - category.equals(dataStoreProvider.getCreationCategory())) + .filter(dataStoreProvider -> category.equals(dataStoreProvider.getCreationCategory())) .map(p -> ProviderEntry.builder() .id(p.getId()) .description(p.getDisplayDescription()) diff --git a/app/src/main/java/io/xpipe/app/ext/ActionProvider.java b/app/src/main/java/io/xpipe/app/ext/ActionProvider.java index a3826007..16f9d109 100644 --- a/app/src/main/java/io/xpipe/app/ext/ActionProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/ActionProvider.java @@ -26,6 +26,107 @@ public interface ActionProvider { } } + default void init() {} + + default String getId() { + return null; + } + + default boolean isActive() { + return true; + } + + default String getProFeatureId() { + return null; + } + + default LauncherCallSite getLauncherCallSite() { + return null; + } + + default DataStoreCallSite getDataStoreCallSite() { + return null; + } + + default DefaultDataStoreCallSite getDefaultDataStoreCallSite() { + return null; + } + + interface Action { + + boolean requiresJavaFXPlatform(); + + void execute() throws Exception; + } + + interface LauncherCallSite { + + String getId(); + + Action createAction(URI uri) throws Exception; + } + + interface XPipeLauncherCallSite extends LauncherCallSite { + + String getId(); + + default Action createAction(URI uri) throws Exception { + var args = new ArrayList<>(Arrays.asList(uri.getPath().substring(1).split("/"))); + args.addFirst(uri.getHost()); + return createAction(args); + } + + Action createAction(List args) throws Exception; + } + + interface DefaultDataStoreCallSite { + + Action createAction(DataStoreEntryRef store); + + Class getApplicableClass(); + + default boolean isApplicable(DataStoreEntryRef o) { + return true; + } + } + + interface DataStoreCallSite { + + default boolean isSystemAction() { + return false; + } + + default boolean canLinkTo() { + return false; + } + + Action createAction(DataStoreEntryRef store); + + Class getApplicableClass(); + + default boolean isMajor(DataStoreEntryRef o) { + return false; + } + + default boolean isApplicable(DataStoreEntryRef o) { + return true; + } + + ObservableValue getName(DataStoreEntryRef store); + + String getIcon(DataStoreEntryRef store); + + default ActiveType activeType() { + return ActiveType.ONLY_SHOW_IF_ENABLED; + } + + enum ActiveType { + ONLY_SHOW_IF_ENABLED, + ALWAYS_SHOW, + ALWAYS_ENABLE + } + } + class Loader implements ModuleLayerLoader { @Override @@ -53,106 +154,4 @@ public interface ActionProvider { return false; } } - - interface Action { - - boolean requiresJavaFXPlatform(); - - void execute() throws Exception; - } - - default void init() { - } - - default String getId() { - return null; - } - - default boolean isActive() { - return true; - } - - default String getProFeatureId() { - return null; - } - - interface LauncherCallSite { - - String getId(); - - Action createAction(URI uri) throws Exception; - } - - interface XPipeLauncherCallSite extends LauncherCallSite { - - String getId(); - - default Action createAction(URI uri) throws Exception { - var args = new ArrayList<>(Arrays.asList(uri.getPath().substring(1).split("/"))); - args.add(0, uri.getHost()); - return createAction(args); - } - - Action createAction(List args) throws Exception; - } - - default LauncherCallSite getLauncherCallSite() { - return null; - } - - default DataStoreCallSite getDataStoreCallSite() { - return null; - } - - default DefaultDataStoreCallSite getDefaultDataStoreCallSite() { - return null; - } - - interface DefaultDataStoreCallSite { - - Action createAction(DataStoreEntryRef store); - - Class getApplicableClass(); - - default boolean isApplicable(DataStoreEntryRef o) { - return true; - } - } - - interface DataStoreCallSite { - - enum ActiveType { - ONLY_SHOW_IF_ENABLED, - ALWAYS_SHOW, - ALWAYS_ENABLE - } - - default boolean isSystemAction() { - return false; - } - - default boolean canLinkTo() { - return false; - } - - Action createAction(DataStoreEntryRef store); - - Class getApplicableClass(); - - default boolean isMajor(DataStoreEntryRef o) { - return false; - } - - default boolean isApplicable(DataStoreEntryRef o) { - return true; - } - - ObservableValue getName(DataStoreEntryRef store); - - String getIcon(DataStoreEntryRef store); - - default ActiveType activeType() { - return ActiveType.ONLY_SHOW_IF_ENABLED; - } - } } 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 29938be9..c2e95c56 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)) { @@ -123,14 +119,13 @@ public interface DataStoreProvider { return null; } - default boolean init() throws Exception { + default boolean init() { return true; } - default void postInit(){ - } + default void postInit() {} - default void storageInit() throws Exception {} + default void storageInit() {} default boolean isShareableFromLocalMachine() { return false; @@ -190,7 +185,7 @@ public interface DataStoreProvider { List getPossibleNames(); default String getId() { - return getPossibleNames().get(0); + return getPossibleNames().getFirst(); } List> getStoreClasses(); diff --git a/app/src/main/java/io/xpipe/app/ext/DataStoreProviders.java b/app/src/main/java/io/xpipe/app/ext/DataStoreProviders.java index 568b88ba..7931ae3e 100644 --- a/app/src/main/java/io/xpipe/app/ext/DataStoreProviders.java +++ b/app/src/main/java/io/xpipe/app/ext/DataStoreProviders.java @@ -62,7 +62,9 @@ public class DataStoreProviders { String.join("_", split), String.join("-", split), split.stream() - .map(s -> s.equals(split.get(0)) ? s : s.substring(0, 1).toUpperCase() + s.substring(1)) + .map(s -> s.equals(split.getFirst()) + ? s + : s.substring(0, 1).toUpperCase() + s.substring(1)) .collect(Collectors.joining())); } 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 20d02e64..8e3f8743 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 d11a82f3..1c634f30 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 b540dc59..14039c65 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; @@ -12,6 +10,22 @@ public abstract class PrefsProvider { private static List ALL; + public static List getAll() { + return ALL; + } + + @SuppressWarnings("unchecked") + public static T get(Class c) { + return (T) ALL.stream() + .filter(prefsProvider -> prefsProvider.getClass().equals(c)) + .findAny() + .orElseThrow(); + } + + public abstract void addPrefs(PrefsHandler handler); + + public abstract void initDefaultValues(); + public static class Loader implements ModuleLayerLoader { @Override @@ -31,25 +45,4 @@ public abstract class PrefsProvider { return false; } } - - public static List getAll() { - return ALL; - } - - @SuppressWarnings("unchecked") - public static T get(Class c) { - return (T) ALL.stream() - .filter(prefsProvider -> prefsProvider.getClass().equals(c)) - .findAny() - .orElseThrow(); - } - - protected > T editable(T o, ObservableBooleanValue v) { - o.editableProperty().bind(v); - return o; - } - - public abstract void addPrefs(PrefsHandler handler); - - public abstract void init(); } diff --git a/app/src/main/java/io/xpipe/app/ext/ScanProvider.java b/app/src/main/java/io/xpipe/app/ext/ScanProvider.java index 3d699950..0815c72f 100644 --- a/app/src/main/java/io/xpipe/app/ext/ScanProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/ScanProvider.java @@ -5,6 +5,7 @@ import io.xpipe.core.process.ShellControl; import io.xpipe.core.store.DataStore; import io.xpipe.core.util.FailableRunnable; import io.xpipe.core.util.ModuleLayerLoader; +import lombok.AllArgsConstructor; import lombok.Value; import java.util.Comparator; @@ -14,15 +15,41 @@ import java.util.stream.Collectors; public abstract class ScanProvider { + private static List ALL; + + public static List getAll() { + return ALL; + } + + public ScanOperation create(DataStore store) { + return null; + } + + public ScanOperation create(DataStoreEntry entry, ShellControl sc) throws Exception { + return null; + } + @Value + @AllArgsConstructor public static class ScanOperation { String nameKey; boolean disabled; boolean defaultSelected; FailableRunnable scanner; - } + String licenseFeatureId; - private static List ALL; + public ScanOperation(String nameKey, boolean disabled, boolean defaultSelected, FailableRunnable scanner) { + this.nameKey = nameKey; + this.disabled = disabled; + this.defaultSelected = defaultSelected; + this.scanner = scanner; + this.licenseFeatureId = null; + } + + public String getLicensedFeatureId() { + return licenseFeatureId; + } + } public static class Loader implements ModuleLayerLoader { @@ -45,16 +72,4 @@ public abstract class ScanProvider { return false; } } - - public static List getAll() { - return ALL; - } - - public ScanOperation create(DataStore store) { - return null; - } - - public ScanOperation create(DataStoreEntry entry, ShellControl sc) throws Exception { - return null; - } } diff --git a/app/src/main/java/io/xpipe/app/ext/XPipeServiceProviders.java b/app/src/main/java/io/xpipe/app/ext/XPipeServiceProviders.java index c551a621..298b7e11 100644 --- a/app/src/main/java/io/xpipe/app/ext/XPipeServiceProviders.java +++ b/app/src/main/java/io/xpipe/app/ext/XPipeServiceProviders.java @@ -6,12 +6,10 @@ import io.xpipe.app.issue.TrackEvent; import io.xpipe.core.process.ProcessControlProvider; import io.xpipe.core.util.JacksonMapper; import io.xpipe.core.util.ModuleLayerLoader; -import io.xpipe.core.util.ProxyFunction; public class XPipeServiceProviders { public static void load(ModuleLayer layer) { - // TODO var hasDaemon = true; ModuleLayerLoader.loadAll(layer, hasDaemon, true, t -> { ErrorEvent.fromThrowable(t).handle(); @@ -33,10 +31,6 @@ public class XPipeServiceProviders { ErrorEvent.fromThrowable(t).handle(); }); - if (hasDaemon) { - ProxyFunction.init(layer); - } - TrackEvent.info("Finished loading extension providers"); } } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/augment/ContextMenuAugment.java b/app/src/main/java/io/xpipe/app/fxcomps/augment/ContextMenuAugment.java index 5744e27c..f0c48ba6 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/augment/ContextMenuAugment.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/augment/ContextMenuAugment.java @@ -10,11 +10,10 @@ import java.util.function.Supplier; public class ContextMenuAugment> implements Augment { + private static ContextMenu currentContextMenu; private final Predicate show; private final Supplier contextMenu; - private static ContextMenu currentContextMenu; - public ContextMenuAugment(Predicate show, Supplier contextMenu) { this.show = show; this.contextMenu = contextMenu; diff --git a/app/src/main/java/io/xpipe/app/fxcomps/augment/DraggableAugment.java b/app/src/main/java/io/xpipe/app/fxcomps/augment/DraggableAugment.java index 19d587ee..170b1b22 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/augment/DraggableAugment.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/augment/DraggableAugment.java @@ -31,7 +31,6 @@ public class DraggableAugment> implements Augment circle.setTranslateY(initialTranslateY + deltaY); lastMouseX = mouseEvent.getSceneX(); lastMouseY = mouseEvent.getSceneY(); - }); circle.setOnMouseEntered(mouseEvent -> { if (!mouseEvent.isPrimaryButtonDown()) { 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 431b83a5..779c5732 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,7 +17,10 @@ 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>> { @@ -37,6 +41,14 @@ public class ChoiceComp extends Comp>> { this.includeNone = includeNone; } + 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); + } + @Override public CompStructure> createBase() { var cb = new ComboBox(); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoicePaneComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoicePaneComp.java index 89b6b862..2bc5f1d5 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoicePaneComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoicePaneComp.java @@ -30,7 +30,7 @@ public class ChoicePaneComp extends Comp> { @Override public CompStructure createBase() { - var list = FXCollections.observableArrayList(entries); + var list = FXCollections.observableArrayList(entries); var cb = new ComboBox<>(list); cb.setOnKeyPressed(event -> { if (!cb.isShowing() && event.getCode().equals(KeyCode.ENTER)) { diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/CodeSnippet.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/CodeSnippet.java index ace9d068..4276ab64 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/CodeSnippet.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/CodeSnippet.java @@ -101,7 +101,7 @@ public record CodeSnippet(List lines) { return this; } - var first = s.lines.get(0); + var first = s.lines.getFirst(); var line = new ArrayList<>(currentLine); line.addAll(first.elements); lines.add(new Line(new ArrayList<>(line))); 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 2b490e0b..07a9cc37 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/ContextualFileReferenceChoiceComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/ContextualFileReferenceChoiceComp.java index 42d86ad3..e4ac6b2c 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/ContextualFileReferenceChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/ContextualFileReferenceChoiceComp.java @@ -37,8 +37,7 @@ public class ContextualFileReferenceChoiceComp extends SimpleComp { private final Property filePath; public ContextualFileReferenceChoiceComp( - ObservableValue> fileSystem, Property filePath - ) { + ObservableValue> fileSystem, Property filePath) { this.fileSystem = new SimpleObjectProperty<>(); SimpleChangeListener.apply(fileSystem, val -> { this.fileSystem.setValue(val); @@ -48,33 +47,39 @@ public class ContextualFileReferenceChoiceComp extends SimpleComp { @Override protected Region createSimple() { - var fileNameComp = new TextFieldComp(filePath).apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS)).styleClass(Styles.LEFT_PILL).grow( - false, true); + var fileNameComp = new TextFieldComp(filePath) + .apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS)) + .styleClass(Styles.LEFT_PILL) + .grow(false, true); var fileBrowseButton = new ButtonComp(null, new FontIcon("mdi2f-folder-open-outline"), () -> { - StandaloneFileBrowser.openSingleFile(() -> fileSystem.getValue(), fileStore -> { - if (fileStore == null) { - filePath.setValue(null); - fileSystem.setValue(null); - } else { - filePath.setValue(fileStore.getPath()); - fileSystem.setValue(fileStore.getFileSystem()); - } - }); - }).styleClass(Styles.CENTER_PILL).grow(false, true); + StandaloneFileBrowser.openSingleFile(() -> fileSystem.getValue(), fileStore -> { + if (fileStore == null) { + filePath.setValue(null); + fileSystem.setValue(null); + } else { + filePath.setValue(fileStore.getPath()); + fileSystem.setValue(fileStore.getFileSystem()); + } + }); + }) + .styleClass(Styles.CENTER_PILL) + .grow(false, true); + var canGitShare = BindingsHelper.persist(Bindings.createBooleanBinding( + () -> { + if (!AppPrefs.get().enableGitStorage().get() + || filePath.getValue() == null + || ContextualFileReference.of(filePath.getValue()).isInDataDirectory()) { + return false; + } - var canGitShare = BindingsHelper.persist(Bindings.createBooleanBinding(() -> { - if (!AppPrefs.get().enableGitStorage().get() || filePath.getValue() == null || ContextualFileReference.of(filePath.getValue()) - .isInDataDirectory()) { - return false; - } - - return true; - }, filePath, AppPrefs.get().enableGitStorage())); + return true; + }, + filePath, + AppPrefs.get().enableGitStorage())); var gitShareButton = new ButtonComp(null, new FontIcon("mdi2g-git"), () -> { - if (filePath.getValue() == null || filePath.getValue().isBlank() || - !canGitShare.get()) { + if (filePath.getValue() == null || filePath.getValue().isBlank() || !canGitShare.get()) { return; } @@ -84,10 +89,12 @@ public class ContextualFileReferenceChoiceComp extends SimpleComp { var source = Path.of(filePath.getValue()); if (Files.exists(source)) { var shouldCopy = AppWindowHelper.showBlockingAlert(alert -> { - alert.setTitle(AppI18n.get("confirmGitShareTitle")); - alert.setHeaderText(AppI18n.get("confirmGitShareHeader")); - alert.setAlertType(Alert.AlertType.CONFIRMATION); - }).map(buttonType -> buttonType.getButtonData().isDefaultButton()).orElse(false); + alert.setTitle(AppI18n.get("confirmGitShareTitle")); + alert.setHeaderText(AppI18n.get("confirmGitShareHeader")); + alert.setAlertType(Alert.AlertType.CONFIRMATION); + }) + .map(buttonType -> buttonType.getButtonData().isDefaultButton()) + .orElse(false); if (!shouldCopy) { return; } 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 5334b23b..76a8c648 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; @@ -33,41 +34,41 @@ import java.util.function.Predicate; @RequiredArgsConstructor public class DataStoreChoiceComp extends SimpleComp { - public static DataStoreChoiceComp other( - Property> selected, Class clazz, Predicate> filter, StoreCategoryWrapper initialCategory) { - return new DataStoreChoiceComp<>(Mode.OTHER, null, selected, clazz, filter, initialCategory); - } - - public static DataStoreChoiceComp proxy(Property> selected, StoreCategoryWrapper initialCategory) { - return new DataStoreChoiceComp<>(Mode.PROXY, null, selected, ShellStore.class, null, initialCategory); - } - - public static DataStoreChoiceComp host(Property> selected, StoreCategoryWrapper initialCategory) { - return new DataStoreChoiceComp<>(Mode.HOST, null, selected, ShellStore.class, null, initialCategory); - } - - public enum Mode { - HOST, - OTHER, - PROXY - } - private final Mode mode; private final DataStoreEntry self; private final Property> selected; private final Class storeClass; private final Predicate> applicableCheck; private final StoreCategoryWrapper initialCategory; - private Popover popover; + public static DataStoreChoiceComp other( + Property> selected, + Class clazz, + Predicate> filter, + StoreCategoryWrapper initialCategory) { + return new DataStoreChoiceComp<>(Mode.OTHER, null, selected, clazz, filter, initialCategory); + } + + public static DataStoreChoiceComp proxy( + Property> selected, StoreCategoryWrapper initialCategory) { + return new DataStoreChoiceComp<>(Mode.PROXY, null, selected, ShellStore.class, null, initialCategory); + } + + public static DataStoreChoiceComp host( + Property> selected, StoreCategoryWrapper initialCategory) { + return new DataStoreChoiceComp<>(Mode.HOST, null, selected, ShellStore.class, null, initialCategory); + } + private Popover getPopover() { // Rebuild popover if we have a non-null condition to allow for the content to be updated in case the condition // changed if (popover == null || applicableCheck != null) { var cur = StoreViewState.get().getActiveCategory().getValue(); var selectedCategory = new SimpleObjectProperty<>( - initialCategory != null ? (initialCategory.getRoot().equals(cur.getRoot()) ? cur : initialCategory) : cur); + initialCategory != null + ? (initialCategory.getRoot().equals(cur.getRoot()) ? cur : initialCategory) + : cur); var filterText = new SimpleStringProperty(); popover = new Popover(); Predicate applicable = storeEntryWrapper -> { @@ -84,8 +85,7 @@ public class DataStoreChoiceComp extends SimpleComp { return storeClass.isAssignableFrom(e.getStore().getClass()) && e.getValidity().isUsable() - && (applicableCheck == null - || applicableCheck.test(e.ref())); + && (applicableCheck == null || applicableCheck.test(e.ref())); }; var section = StoreSectionMiniComp.createList( StoreSection.createTopLevel( @@ -101,11 +101,13 @@ public class DataStoreChoiceComp extends SimpleComp { comp.disable(new SimpleBooleanProperty(true)); } }); - var category = new DataStoreCategoryChoiceComp(initialCategory != null ? initialCategory.getRoot() : null, StoreViewState.get().getActiveCategory(), - selectedCategory).styleClass(Styles.LEFT_PILL); - var filter = new FilterComp(filterText) - .styleClass(Styles.CENTER_PILL) - .hgrow(); + var category = new DataStoreCategoryChoiceComp( + initialCategory != null ? initialCategory.getRoot() : null, + StoreViewState.get().getActiveCategory(), + selectedCategory) + .styleClass(Styles.LEFT_PILL); + var filter = + new FilterComp(filterText).styleClass(Styles.CENTER_PILL).hgrow(); var addButton = Comp.of(() -> { MenuButton m = new MenuButton(null, new FontIcon("mdi2p-plus-box-outline")); @@ -123,13 +125,21 @@ public class DataStoreChoiceComp extends SimpleComp { .apply(struc -> { // Ugly solution to focus the text field // Somehow this does not work through the normal on shown listeners - struc.get().getChildren().get(0).focusedProperty().addListener((observable, oldValue, newValue) -> { - if (newValue) { - ((StackPane) struc.get().getChildren().get(1)).getChildren().get(1).requestFocus(); - } - }); + struc.get() + .getChildren() + .get(0) + .focusedProperty() + .addListener((observable, oldValue, newValue) -> { + if (newValue) { + ((StackPane) struc.get().getChildren().get(1)) + .getChildren() + .get(1) + .requestFocus(); + } + }); }) - .createStructure().get(); + .createStructure() + .get(); var r = section.vgrow().createRegion(); var content = new VBox(top, r); content.setFillWidth(true); @@ -154,9 +164,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"); } @@ -168,7 +176,9 @@ public class DataStoreChoiceComp extends SimpleComp { var button = new ButtonComp( Bindings.createStringBinding( () -> { - return selected.getValue() != null ? toName(selected.getValue().getEntry()) : null; + return selected.getValue() != null + ? toName(selected.getValue().getEntry()) + : null; }, selected), () -> {}); @@ -214,4 +224,10 @@ public class DataStoreChoiceComp extends SimpleComp { r.maxWidthProperty().bind(pane.widthProperty()); return pane; } + + public enum Mode { + HOST, + OTHER, + PROXY + } } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreListChoiceComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreListChoiceComp.java index 946165b6..dffe8598 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreListChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/DataStoreListChoiceComp.java @@ -22,9 +22,11 @@ public class DataStoreListChoiceComp extends SimpleComp { private final Predicate> applicableCheck; private final StoreCategoryWrapper initialCategory; - public DataStoreListChoiceComp(ListProperty> selectedList, Class storeClass, Predicate> applicableCheck, - StoreCategoryWrapper initialCategory - ) { + public DataStoreListChoiceComp( + ListProperty> selectedList, + Class storeClass, + Predicate> applicableCheck, + StoreCategoryWrapper initialCategory) { this.selectedList = selectedList; this.storeClass = storeClass; this.applicableCheck = applicableCheck; @@ -34,30 +36,35 @@ public class DataStoreListChoiceComp extends SimpleComp { @Override protected Region createSimple() { var list = new ListBoxViewComp<>(selectedList, selectedList, t -> { - if (t == null) { - return null; - } + if (t == null) { + return null; + } - var label = new LabelComp(t.get().getName()).apply(struc -> struc.get().setGraphic(PrettyImageHelper.ofFixedSmallSquare( - t.get().getProvider().getDisplayIconFileName(t.getStore())).createRegion())); - var delete = new IconButtonComp("mdal-delete_outline", () -> { - selectedList.remove(t); - }); - return new HorizontalComp(List.of(label, Comp.hspacer(), delete)).styleClass("entry"); - }).padding(new Insets(0)).apply(struc -> struc.get().setMinHeight(0)).apply(struc -> ((VBox) struc.get().getContent()).setSpacing(5)); + var label = new LabelComp(t.get().getName()).apply(struc -> struc.get() + .setGraphic(PrettyImageHelper.ofFixedSmallSquare( + t.get().getProvider().getDisplayIconFileName(t.getStore())) + .createRegion())); + var delete = new IconButtonComp("mdal-delete_outline", () -> { + selectedList.remove(t); + }); + return new HorizontalComp(List.of(label, Comp.hspacer(), delete)).styleClass("entry"); + }) + .padding(new Insets(0)) + .apply(struc -> struc.get().setMinHeight(0)) + .apply(struc -> ((VBox) struc.get().getContent()).setSpacing(5)); var selected = new SimpleObjectProperty>(); - var add = new DataStoreChoiceComp<>(DataStoreChoiceComp.Mode.OTHER, null, selected, storeClass, applicableCheck, initialCategory); + var add = new DataStoreChoiceComp<>( + DataStoreChoiceComp.Mode.OTHER, null, selected, storeClass, applicableCheck, initialCategory); selected.addListener((observable, oldValue, newValue) -> { if (newValue != null) { - if (!selectedList.contains(newValue) - && (applicableCheck == null - || applicableCheck.test(newValue))) { + if (!selectedList.contains(newValue) && (applicableCheck == null || applicableCheck.test(newValue))) { selectedList.add(newValue); } selected.setValue(null); } }); - var vbox = new VerticalComp(List.of(list, Comp.vspacer(5), add)).apply(struc -> struc.get().setFillWidth(true)); + var vbox = new VerticalComp(List.of(list, Comp.vspacer(5), add)) + .apply(struc -> struc.get().setFillWidth(true)); return vbox.styleClass("data-store-list-choice-comp").createRegion(); } } 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 34cdbef4..c6c1705d 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/IntFieldComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/IntFieldComp.java index 8615c7ba..0beffef9 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/IntFieldComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/IntFieldComp.java @@ -67,7 +67,10 @@ public class IntFieldComp extends Comp> { }); text.textProperty().addListener((observableValue, oldValue, newValue) -> { - if (newValue == null || newValue.isEmpty() || (minValue < 0 && "-".equals(newValue)) || !newValue.matches("-?\\d+")) { + if (newValue == null + || newValue.isEmpty() + || (minValue < 0 && "-".equals(newValue)) + || !newValue.matches("-?\\d+")) { value.setValue(null); return; } 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 2e626aef..1021cd87 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 @@ -12,7 +12,6 @@ import io.xpipe.app.fxcomps.util.PlatformThread; import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.value.ObservableValue; -import javafx.geometry.Insets; import javafx.geometry.Orientation; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -50,13 +49,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; } @@ -100,10 +99,9 @@ public class OptionsComp extends Comp> { extendedDescription.setMinWidth(Region.USE_PREF_SIZE); extendedDescription.getStyleClass().add(Styles.BUTTON_OUTLINED); extendedDescription.getStyleClass().add(Styles.ACCENT); - extendedDescription.setPadding(new Insets(0, 6, 0, 6)); extendedDescription.getStyleClass().add("long-description"); extendedDescription.setAccessibleText("Help"); - AppFont.header(extendedDescription); + AppFont.normal(extendedDescription); extendedDescription.setOnAction(e -> { popover.show(extendedDescription); e.consume(); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/PrettyImageComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/PrettyImageComp.java index c7689b27..74ad4dde 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/PrettyImageComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/PrettyImageComp.java @@ -79,7 +79,9 @@ public class PrettyImageComp extends SimpleComp { } else if (AppImages.hasNormalImage(image.getValue().replace("-dark", ""))) { return AppImages.image(image.getValue().replace("-dark", "")); } else { - TrackEvent.withWarn("Image file not found").tag("file",image.getValue()).handle(); + TrackEvent.withWarn("Image file not found") + .tag("file", image.getValue()) + .handle(); return null; } }, @@ -89,15 +91,17 @@ public class PrettyImageComp extends SimpleComp { storeIcon.setSmooth(true); stack.getChildren().add(storeIcon); - Consumer update = val -> { - var fixed = val != null ? FileNames.getBaseName(val) + (AppPrefs.get().theme.get().isDark() ? "-dark" : "") + "." + FileNames.getExtension(val) : null; + var fixed = val != null + ? FileNames.getBaseName(val) + (AppPrefs.get().theme.get().isDark() ? "-dark" : "") + "." + + FileNames.getExtension(val) + : null; image.set(fixed); if (val == null) { - stack.getChildren().get(0).setVisible(false); + stack.getChildren().getFirst().setVisible(false); } else { - stack.getChildren().get(0).setVisible(true); + stack.getChildren().getFirst().setVisible(true); } }; diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/PrettyImageHelper.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/PrettyImageHelper.java index 7a7e6d44..1aaf75cb 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/PrettyImageHelper.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/PrettyImageHelper.java @@ -35,20 +35,21 @@ public class PrettyImageHelper { if (rasterized.isPresent()) { return rasterized.get(); } else { - return img.endsWith(".svg") ? new PrettySvgComp(new SimpleStringProperty(img), w, h) : new PrettyImageComp(new SimpleStringProperty(img), w, h); + return img.endsWith(".svg") + ? new PrettySvgComp(new SimpleStringProperty(img), w, h) + : new PrettyImageComp(new SimpleStringProperty(img), w, h); } } - public static Comp ofSvg(ObservableValue img, int w, int h) { + public static Comp ofSvg(ObservableValue img, int w, int h) { return new PrettySvgComp(img, w, h); } - public static Comp ofRasterized(ObservableValue img, int w, int h) { + public static Comp ofRasterized(ObservableValue img, int w, int h) { return new PrettyImageComp(img, w, h); } - public static Comp ofFixedSmallSquare(String img) { + public static Comp ofFixedSmallSquare(String img) { return ofFixedSize(img, 16, 16); } - } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/PrettySvgComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/PrettySvgComp.java index bb1a1939..2519304c 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/PrettySvgComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/PrettySvgComp.java @@ -85,7 +85,10 @@ public class PrettySvgComp extends SimpleComp { stack.getChildren().add(node); Consumer update = val -> { - var fixed = val != null ? FileNames.getBaseName(val) + (AppPrefs.get().theme.get().isDark() ? "-dark" : "") + "." + FileNames.getExtension(val) : null; + var fixed = val != null + ? FileNames.getBaseName(val) + (AppPrefs.get().theme.get().isDark() ? "-dark" : "") + "." + + FileNames.getExtension(val) + : null; image.set(fixed); }; 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 29e57e5b..387e2ef0 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; + private final Property value; - public SecretFieldComp(Property value) { + public SecretFieldComp(Property value) { this.value = value; } - protected SecretValue encrypt(char[] c) { - return SecretHelper.encrypt(c); + 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); + } + + protected InPlaceSecretValue encrypt(char[] c) { + return InPlaceSecretValue.of(c); } @Override @@ -35,13 +47,15 @@ public class SecretFieldComp extends Comp> { value.addListener((c, o, n) -> { PlatformThread.runLaterIfNeeded(() -> { // Check if control value is the same. Then don't set it as that might cause bugs - if ((n == null && text.getText().isEmpty()) || Objects.equals(text.getText(), n != null ? n.getSecretValue() : null)) { + if ((n == null && text.getText().isEmpty()) + || Objects.equals(text.getText(), n != null ? n.getSecretValue() : null)) { return; } 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/StoreCategoryComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java index b823d742..d44e6e64 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java @@ -42,52 +42,75 @@ public class StoreCategoryComp extends SimpleComp { @Override protected Region createSimple() { - var i = Bindings.createStringBinding(() -> { - if (!DataStorage.get().supportsSharing() || !category.getCategory().canShare()) { - return "mdal-keyboard_arrow_right"; - } + var i = Bindings.createStringBinding( + () -> { + if (!DataStorage.get().supportsSharing() + || !category.getCategory().canShare()) { + return "mdal-keyboard_arrow_right"; + } - return category.getShare().getValue() ? "mdi2a-account-convert" : "mdi2a-account-cancel"; - }, category.getShare()); - var icon = new IconButtonComp(i).apply(struc -> AppFont.small(struc.get())).apply(struc -> { - struc.get().setAlignment(Pos.CENTER); - struc.get().setPadding(new Insets(0, 0, 6, 0)); - struc.get().setFocusTraversable(false); - }); - var name = new LazyTextFieldComp(category.nameProperty()).apply(struc -> { - struc.get().prefWidthProperty().unbind(); - struc.get().setPrefWidth(150); - struc.getTextField().minWidthProperty().bind(struc.get().widthProperty()); - }).styleClass("name").createRegion(); + return category.getShare().getValue() ? "mdi2a-account-convert" : "mdi2a-account-cancel"; + }, + category.getShare()); + var icon = new IconButtonComp(i) + .apply(struc -> AppFont.small(struc.get())) + .apply(struc -> { + struc.get().setAlignment(Pos.CENTER); + struc.get().setPadding(new Insets(0, 0, 6, 0)); + struc.get().setFocusTraversable(false); + }); + var name = new LazyTextFieldComp(category.nameProperty()) + .apply(struc -> { + struc.get().prefWidthProperty().unbind(); + struc.get().setPrefWidth(150); + struc.getTextField().minWidthProperty().bind(struc.get().widthProperty()); + }) + .styleClass("name") + .createRegion(); var showing = new SimpleBooleanProperty(); - var settings = new IconButtonComp("mdomz-settings").styleClass("settings").apply( - new ContextMenuAugment<>(mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY, () -> { + var settings = new IconButtonComp("mdomz-settings") + .styleClass("settings") + .apply(new ContextMenuAugment<>(mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY, () -> { var cm = createContextMenu(name); showing.bind(cm.showingProperty()); return cm; })); - var shownList = BindingsHelper.filteredContentBinding(category.getContainedEntries(), storeEntryWrapper -> { - return storeEntryWrapper.shouldShow(StoreViewState.get().getFilterString().getValue()); - }, StoreViewState.get().getFilterString()); + var shownList = BindingsHelper.filteredContentBinding( + category.getContainedEntries(), + storeEntryWrapper -> { + return storeEntryWrapper.shouldShow( + StoreViewState.get().getFilterString().getValue()); + }, + StoreViewState.get().getFilterString()); var count = new CountComp<>(shownList, category.getContainedEntries(), string -> "(" + string + ")"); var hover = new SimpleBooleanProperty(); var focus = new SimpleBooleanProperty(); - var h = new HorizontalComp( - List.of(icon, Comp.hspacer(4), Comp.of(() -> name), Comp.hspacer(), count.hide(BindingsHelper.persist(hover.or(showing).or(focus))), - settings.hide(BindingsHelper.persist(hover.not().and(showing.not()).and(focus.not()))))); - h.apply(new ContextMenuAugment<>(mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY, () -> createContextMenu(name))); + var h = new HorizontalComp(List.of( + icon, + Comp.hspacer(4), + Comp.of(() -> name), + Comp.hspacer(), + count.hide(BindingsHelper.persist(hover.or(showing).or(focus))), + settings.hide( + BindingsHelper.persist(hover.not().and(showing.not()).and(focus.not()))))); + h.apply(new ContextMenuAugment<>( + mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY, () -> createContextMenu(name))); h.padding(new Insets(0, 10, 0, (category.getDepth() * 10))); - var categoryButton = new ButtonComp(null, h.createRegion(), category::select).styleClass("category-button").apply( - struc -> hover.bind(struc.get().hoverProperty())).apply(struc -> focus.bind(struc.get().focusedProperty())).accessibleText( - category.nameProperty()).grow(true, false); + var categoryButton = new ButtonComp(null, h.createRegion(), category::select) + .styleClass("category-button") + .apply(struc -> hover.bind(struc.get().hoverProperty())) + .apply(struc -> focus.bind(struc.get().focusedProperty())) + .accessibleText(category.nameProperty()) + .grow(true, false); - var l = category.getChildren().sorted(Comparator.comparing(storeCategoryWrapper -> storeCategoryWrapper.getName().toLowerCase(Locale.ROOT))); + var l = category.getChildren() + .sorted(Comparator.comparing( + storeCategoryWrapper -> storeCategoryWrapper.getName().toLowerCase(Locale.ROOT))); var children = new ListBoxViewComp<>(l, l, storeCategoryWrapper -> new StoreCategoryComp(storeCategoryWrapper)); var emptyBinding = Bindings.isEmpty(category.getChildren()); - var v = new VerticalComp( - List.of(categoryButton, children.hide(emptyBinding))); + var v = new VerticalComp(List.of(categoryButton, children.hide(emptyBinding))); v.styleClass("category"); v.apply(struc -> { SimpleChangeListener.apply(StoreViewState.get().getActiveCategory(), val -> { @@ -104,26 +127,34 @@ public class StoreCategoryComp extends SimpleComp { var newCategory = new MenuItem(AppI18n.get("newCategory"), new FontIcon("mdi2p-plus-thick")); newCategory.setOnAction(event -> { - DataStorage.get().addStoreCategory(DataStoreCategory.createNew(category.getCategory().getUuid(), "New category")); + DataStorage.get() + .addStoreCategory( + DataStoreCategory.createNew(category.getCategory().getUuid(), "New category")); }); contextMenu.getItems().add(newCategory); if (DataStorage.get().supportsSharing() && category.getCategory().canShare()) { var share = new MenuItem(); - share.textProperty().bind(Bindings.createStringBinding(() -> { - if (category.getShare().getValue()) { - return AppI18n.get("unshare"); - } else { - return AppI18n.get("share"); - } - }, category.getShare())); - share.graphicProperty().bind(Bindings.createObjectBinding(() -> { - if (category.getShare().getValue()) { - return new FontIcon("mdi2b-block-helper"); - } else { - return new FontIcon("mdi2s-share"); - } - }, category.getShare())); + share.textProperty() + .bind(Bindings.createStringBinding( + () -> { + if (category.getShare().getValue()) { + return AppI18n.get("unshare"); + } else { + return AppI18n.get("share"); + } + }, + category.getShare())); + share.graphicProperty() + .bind(Bindings.createObjectBinding( + () -> { + if (category.getShare().getValue()) { + return new FontIcon("mdi2b-block-helper"); + } else { + return new FontIcon("mdi2s-share"); + } + }, + category.getShare())); share.setOnAction(event -> { category.getShare().setValue(!category.getShare().getValue()); }); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryListComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryListComp.java index 7d3caec7..4339903b 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryListComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryListComp.java @@ -16,8 +16,7 @@ public class StoreCategoryListComp extends SimpleComp { @Override protected Region createSimple() { - return new VerticalComp(List.of( - new StoreCategoryComp(root))) + return new VerticalComp(List.of(new StoreCategoryComp(root))) .apply(struc -> struc.get().setFillWidth(true)) .apply(struc -> struc.get().setSpacing(3)) .styleClass("store-category-bar") diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/SvgView.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/SvgView.java index bb284cd4..f5960527 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/SvgView.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/SvgView.java @@ -55,7 +55,9 @@ public class SvgView { private WebView createWebView() { var wv = new WebView(); - wv.getEngine().setUserDataDirectory(AppProperties.get().getDataDir().resolve("webview").toFile()); + wv.getEngine() + .setUserDataDirectory( + AppProperties.get().getDataDir().resolve("webview").toFile()); // Sometimes a web view might not render when the background is set to transparent, at least according to stack // overflow wv.setPageFill(Color.valueOf("#00000001")); @@ -67,7 +69,7 @@ public class SvgView { wv.setDisable(true); wv.getEngine().loadContent(svgContent.getValue() != null ? getHtml(svgContent.getValue()) : null); - SimpleChangeListener.apply(svgContent, n -> { + SimpleChangeListener.apply(svgContent, n -> { if (n == null) { wv.setOpacity(0.0); return; diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/TextAreaComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/TextAreaComp.java index 40d3bf62..9f0ffe0c 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/TextAreaComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/TextAreaComp.java @@ -16,18 +16,6 @@ import java.util.Objects; public class TextAreaComp extends Comp { - @Value - @Builder - public static class Structure implements CompStructure { - AnchorPane pane; - TextArea textArea; - - @Override - public AnchorPane get() { - return pane; - } - } - private final Property currentValue; private final Property lastAppliedValue; private final boolean lazy; @@ -95,4 +83,16 @@ public class TextAreaComp extends Comp { return new Structure(anchorPane, text); } + + @Value + @Builder + public static class Structure implements CompStructure { + AnchorPane pane; + TextArea textArea; + + @Override + public AnchorPane get() { + return pane; + } + } } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/ToggleGroupComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/ToggleGroupComp.java index ddabf603..55cd6f56 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/ToggleGroupComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/ToggleGroupComp.java @@ -56,14 +56,11 @@ public class ToggleGroupComp extends Comp> { } if (box.getChildren().size() > 0) { - box.getChildren().get(0).getStyleClass().add(Styles.LEFT_PILL); + box.getChildren().getFirst().getStyleClass().add(Styles.LEFT_PILL); for (int i = 1; i < box.getChildren().size() - 1; i++) { box.getChildren().get(i).getStyleClass().add(Styles.CENTER_PILL); } - box.getChildren() - .get(box.getChildren().size() - 1) - .getStyleClass() - .add(Styles.RIGHT_PILL); + box.getChildren().getLast().getStyleClass().add(Styles.RIGHT_PILL); } }); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/util/BindingsHelper.java b/app/src/main/java/io/xpipe/app/fxcomps/util/BindingsHelper.java index d5133514..b557a61d 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/util/BindingsHelper.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/util/BindingsHelper.java @@ -23,27 +23,10 @@ import java.util.function.Predicate; public class BindingsHelper { private static final Set REFERENCES = Collections.newSetFromMap(new ConcurrentHashMap<>()); - - public static void bindExclusive( - Property selected, Map> map, Property toBind) { - selected.addListener((c, o, n) -> { - toBind.unbind(); - toBind.bind(map.get(n)); - }); - - toBind.bind(map.get(selected.getValue())); - } - - @Value - private static class ReferenceEntry { - - WeakReference source; - Object target; - - public boolean canGc() { - return source.get() == null; - } - } + /* + TODO: Proper cleanup. Maybe with a separate thread? + */ + private static final Map, Set> BINDINGS = new ConcurrentHashMap<>(); static { ThreadHelper.createPlatformThread("referenceGC", true, () -> { @@ -62,15 +45,20 @@ public class BindingsHelper { .start(); } + public static void bindExclusive( + Property selected, Map> map, Property toBind) { + selected.addListener((c, o, n) -> { + toBind.unbind(); + toBind.bind(map.get(n)); + }); + + toBind.bind(map.get(selected.getValue())); + } + public static void linkPersistently(Object source, Object target) { REFERENCES.add(new ReferenceEntry(new WeakReference<>(source), target)); } - /* - TODO: Proper cleanup. Maybe with a separate thread? - */ - private static final Map, Set> BINDINGS = new ConcurrentHashMap<>(); - public static > T persist(T binding) { var dependencies = new HashSet(); while (dependencies.addAll(binding.getDependencies().stream() @@ -98,13 +86,17 @@ public class BindingsHelper { }); } - public static ObservableValue map(ObservableValue observableValue, Function mapper) { - return persist(Bindings.createObjectBinding(() -> { - return mapper.apply(observableValue.getValue()); - }, observableValue)); + public static ObservableValue map( + ObservableValue observableValue, Function mapper) { + return persist(Bindings.createObjectBinding( + () -> { + return mapper.apply(observableValue.getValue()); + }, + observableValue)); } - public static ObservableValue flatMap(ObservableValue observableValue, Function> mapper) { + public static ObservableValue flatMap( + ObservableValue observableValue, Function> mapper) { var prop = new SimpleObjectProperty(); Runnable runnable = () -> { prop.bind(mapper.apply(observableValue.getValue())); @@ -117,10 +109,12 @@ public class BindingsHelper { return prop; } - public static ObservableValue anyMatch(List> l) { - return BindingsHelper.persist(Bindings.createBooleanBinding(() -> { - return l.stream().anyMatch(booleanObservableValue -> booleanObservableValue.getValue()); - }, l.toArray(ObservableValue[]::new))); + public static ObservableValue anyMatch(List> l) { + return BindingsHelper.persist(Bindings.createBooleanBinding( + () -> { + return l.stream().anyMatch(booleanObservableValue -> booleanObservableValue.getValue()); + }, + l.toArray(ObservableValue[]::new))); } public static void bindMappedContent(ObservableList l1, ObservableList l2, Function map) { @@ -152,14 +146,17 @@ public class BindingsHelper { ObservableList l1 = FXCollections.observableList(new ArrayList<>()); Runnable runnable = () -> { cache.keySet().removeIf(t -> !l2.contains(t)); - setContent(l1, l2.stream() - .map(v -> { - if (!cache.containsKey(v)) { - cache.put(v, map.apply(v)); - } + setContent( + l1, + l2.stream() + .map(v -> { + if (!cache.containsKey(v)) { + cache.put(v, map.apply(v)); + } - return cache.get(v); - }).toList()); + return cache.get(v); + }) + .toList()); }; runnable.run(); l2.addListener((ListChangeListener) c -> { @@ -169,20 +166,24 @@ public class BindingsHelper { return l1; } - public static ObservableList cachedMappedContentBinding(ObservableList all, ObservableList shown, Function map) { + public static ObservableList cachedMappedContentBinding( + ObservableList all, ObservableList shown, Function map) { var cache = new HashMap(); ObservableList l1 = FXCollections.observableList(new ArrayList<>()); Runnable runnable = () -> { cache.keySet().removeIf(t -> !all.contains(t)); - setContent(l1, shown.stream() - .map(v -> { - if (!cache.containsKey(v)) { - cache.put(v, map.apply(v)); - } + setContent( + l1, + shown.stream() + .map(v -> { + if (!cache.containsKey(v)) { + cache.put(v, map.apply(v)); + } - return cache.get(v); - }).toList()); + return cache.get(v); + }) + .toList()); }; runnable.run(); shown.addListener((ListChangeListener) c -> { @@ -193,32 +194,40 @@ public class BindingsHelper { return l1; } - public static ObservableValue mappedBinding(ObservableValue observableValue, Function> mapper) { + public static ObservableValue mappedBinding( + ObservableValue observableValue, Function> mapper) { var binding = (Binding) observableValue.flatMap(mapper); return persist(binding); } -// public static ObservableValue mappedBinding(ObservableValue observableValue, Function> mapper) { -// var v = new SimpleObjectProperty(); -// SimpleChangeListener.apply(observableValue, val -> { -// v.unbind(); -// v.bind(mapper.apply(val)); -// }); -// return v; -// } - - public static ObservableList orderedContentBinding(ObservableList l2, Comparator comp, Observable... observables) { - return orderedContentBinding(l2, Bindings.createObjectBinding(() -> { - return new Comparator<>() { - @Override - public int compare(V o1, V o2) { - return comp.compare(o1, o2); - } - }; - }, observables)); + public static ObservableList orderedContentBinding( + ObservableList l2, Comparator comp, Observable... observables) { + return orderedContentBinding( + l2, + Bindings.createObjectBinding( + () -> { + return new Comparator<>() { + @Override + public int compare(V o1, V o2) { + return comp.compare(o1, o2); + } + }; + }, + observables)); } - public static ObservableList orderedContentBinding(ObservableList l2, ObservableValue> comp) { + // public static ObservableValue mappedBinding(ObservableValue observableValue, Function> mapper) { + // var v = new SimpleObjectProperty(); + // SimpleChangeListener.apply(observableValue, val -> { + // v.unbind(); + // v.bind(mapper.apply(val)); + // }); + // return v; + // } + + public static ObservableList orderedContentBinding( + ObservableList l2, ObservableValue> comp) { ObservableList l1 = FXCollections.observableList(new ArrayList<>()); Runnable runnable = () -> { setContent(l1, l2.stream().sorted(comp.getValue()).toList()); @@ -238,7 +247,8 @@ public class BindingsHelper { return filteredContentBinding(l2, new SimpleObjectProperty<>(predicate)); } - public static ObservableList filteredContentBinding(ObservableList l2, Predicate predicate, Observable... observables) { + public static ObservableList filteredContentBinding( + ObservableList l2, Predicate predicate, Observable... observables) { return filteredContentBinding( l2, Bindings.createObjectBinding( @@ -250,14 +260,18 @@ public class BindingsHelper { } }; }, - Arrays.stream(observables).filter( Objects::nonNull).toArray(Observable[]::new))); + Arrays.stream(observables).filter(Objects::nonNull).toArray(Observable[]::new))); } public static ObservableList filteredContentBinding( ObservableList l2, ObservableValue> predicate) { ObservableList l1 = FXCollections.observableList(new ArrayList<>()); Runnable runnable = () -> { - setContent(l1, predicate.getValue() != null ? l2.stream().filter(predicate.getValue()).toList() : l2); + setContent( + l1, + predicate.getValue() != null + ? l2.stream().filter(predicate.getValue()).toList() + : l2); }; runnable.run(); l2.addListener((ListChangeListener) c -> { @@ -313,4 +327,15 @@ public class BindingsHelper { // Other cases are more difficult target.setAll(newList); } + + @Value + private static class ReferenceEntry { + + WeakReference source; + Object target; + + public boolean canGc() { + return source.get() == null; + } + } } diff --git a/app/src/main/java/io/xpipe/app/issue/AttachmentHelper.java b/app/src/main/java/io/xpipe/app/issue/AttachmentHelper.java index 0ff47d5e..39df1e49 100644 --- a/app/src/main/java/io/xpipe/app/issue/AttachmentHelper.java +++ b/app/src/main/java/io/xpipe/app/issue/AttachmentHelper.java @@ -21,7 +21,12 @@ public class AttachmentHelper { private static void compressDirectoryToZipfile(Path rootDir, Path sourceDir, ZipOutputStream out) throws IOException { - for (File file : sourceDir.toFile().listFiles()) { + var files = sourceDir.toFile().listFiles(); + if (files == null) { + return; + } + + for (File file : files) { if (file.isDirectory()) { compressDirectoryToZipfile(rootDir, sourceDir.resolve(file.getName()), out); } else { 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 3b7a114b..9fc1a9d5 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 a53e98ca..e921d4be 100644 --- a/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java +++ b/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java @@ -8,13 +8,14 @@ import lombok.Singular; import java.nio.file.Path; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; @Builder @Getter public class ErrorEvent { - private String description; - private boolean terminal; + private static final Map EVENT_BASES = new ConcurrentHashMap<>(); + private static final Set HANDLED = new CopyOnWriteArraySet<>(); @Builder.Default private final boolean omitted = false; @@ -24,6 +25,12 @@ public class ErrorEvent { private final Throwable throwable; + @Singular + private final List customActions; + + private String description; + private boolean terminal; + @Setter private boolean shouldSendDiagnostics; @@ -32,14 +39,7 @@ public class ErrorEvent { private String email; private String userReport; - - @Singular - private final List customActions; - - public void attachUserReport(String email, String text) { - this.email = email; - userReport = text; - } + private boolean unhandled; public static ErrorEventBuilder fromThrowable(Throwable t) { if (EVENT_BASES.containsKey(t)) { @@ -61,12 +61,70 @@ public class ErrorEvent { return builder().description(msg); } - public void handle() { - EventHandler.get().modify(this); - EventHandler.get().handle(this); + public static T unreportableIfEndsWith(T t, String... s) { + return unreportableIf( + t, + t.getMessage() != null + && Arrays.stream(s).map(String::toLowerCase).anyMatch(string -> t.getMessage() + .toLowerCase(Locale.ROOT) + .endsWith(string))); } + public static T unreportableIfContains(T t, String... s) { + return unreportableIf( + t, + t.getMessage() != null + && Arrays.stream(s).map(String::toLowerCase).anyMatch(string -> t.getMessage() + .toLowerCase(Locale.ROOT) + .contains(string))); + } + public static T unreportableIf(T t, boolean b) { + if (b) { + EVENT_BASES.put(t, ErrorEvent.fromThrowable(t).expected()); + } + return t; + } + + public static T unreportable(T t) { + EVENT_BASES.put(t, ErrorEvent.fromThrowable(t).expected()); + return t; + } + + public static void preconfigure(ErrorEventBuilder event) { + EVENT_BASES.put(event.throwable, event); + } + + public void attachUserReport(String email, String text) { + this.email = email; + userReport = text; + } + + 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); @@ -99,36 +157,4 @@ public class ErrorEvent { build().handle(); } } - - private static final Map EVENT_BASES = new ConcurrentHashMap<>(); - - public static T unreportableIfEndsWith(T t, String... s) { - return unreportableIf( - t, - t.getMessage() != null - && Arrays.stream(s).map(String::toLowerCase).anyMatch(string -> t.getMessage() - .toLowerCase(Locale.ROOT) - .endsWith(string))); - } - - public static T unreportableIfContains(T t, String... s) { - return unreportableIf( - t, - t.getMessage() != null - && Arrays.stream(s).map(String::toLowerCase).anyMatch(string -> t.getMessage() - .toLowerCase(Locale.ROOT) - .contains(string))); - } - - public static T unreportableIf(T t, boolean b) { - if (b) { - EVENT_BASES.put(t, ErrorEvent.fromThrowable(t).expected()); - } - return t; - } - - public static T unreportable(T t) { - EVENT_BASES.put(t, ErrorEvent.fromThrowable(t).expected()); - return t; - } } diff --git a/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java b/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java index 2caabe61..e15e2209 100644 --- a/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java +++ b/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java @@ -53,6 +53,12 @@ public class ErrorHandlerComp extends SimpleComp { return; } + // Unhandled platform exceptions usually means that we will have trouble displaying another window + if (event.isUnhandled() && Platform.isFxApplicationThread()) { + ErrorAction.ignore().handle(event); + return; + } + if (Platform.isFxApplicationThread()) { showAndWaitWithPlatformThread(event, forceWait); } else { @@ -106,11 +112,15 @@ public class ErrorHandlerComp extends SimpleComp { Platform.runLater(() -> { if (!showing.get()) { showing.set(true); - Stage window = null; + Stage window; try { - window = AppWindowHelper.sideWindow(AppI18n.get("errorHandler"), w -> { - return setUpComp(event, w, finishLatch); - }, true, null); + window = AppWindowHelper.sideWindow( + AppI18n.get("errorHandler"), + w -> { + return setUpComp(event, w, finishLatch); + }, + true, + null); } catch (Throwable t) { showLatch.countDown(); finishLatch.countDown(); @@ -234,8 +244,7 @@ public class ErrorHandlerComp extends SimpleComp { actionBox.getChildren().add(ac); } - for (var action : - List.of(ErrorAction.ignore())) { + for (var action : List.of(ErrorAction.ignore())) { var ac = createActionComp(action); actionBox.getChildren().add(ac); } 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 ebec2869..55c6813a 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 2751c06b..81096d6f 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,11 @@ 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 98163bf9..878e4549 100644 --- a/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java +++ b/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java @@ -18,62 +18,18 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.nio.file.FileSystemException; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.util.stream.Collectors; public class SentryErrorHandler implements ErrorHandler { private static final ErrorHandler INSTANCE = new SyncErrorHandler(new SentryErrorHandler()); + private boolean init; public static ErrorHandler getInstance() { return INSTANCE; } - private boolean init; - - public void handle(ErrorEvent ee) { - // Assume that this object is wrapped by a synchronous error handler - if (!init) { - AppProperties.init(); - AppState.init(); - if (AppProperties.get().getSentryUrl() != null) { - Sentry.init(options -> { - options.setDsn(AppProperties.get().getSentryUrl()); - options.setEnableUncaughtExceptionHandler(false); - options.setAttachServerName(false); - options.setRelease(AppProperties.get().getVersion()); - options.setEnableShutdownHook(false); - options.setProguardUuid(AppProperties.get().getBuildUuid().toString()); - options.setTag("os", System.getProperty("os.name")); - options.setTag("osVersion", System.getProperty("os.version")); - 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.setAttachThreads(false); - }); - } - init = true; - } - - var id = captureEvent(ee); - if (id == null) { - return; - } - - var email = ee.getEmail(); - var hasEmail = email != null && !email.isBlank(); - var text = ee.getUserReport(); - if (hasUserReport(ee)) { - var fb = new UserFeedback(id); - if (hasEmail) { - fb.setEmail(email); - } - fb.setComments(text); - Sentry.captureUserFeedback(fb); - } - Sentry.flush(3000); - } - private static boolean hasUserReport(ErrorEvent ee) { var email = ee.getEmail(); var hasEmail = email != null && !email.isBlank(); @@ -114,6 +70,12 @@ public class SentryErrorHandler implements ErrorHandler { otherField.set(copy, null); } + if (copy instanceof InvalidPathException) { + var inputField = InvalidPathException.class.getDeclaredField("input"); + inputField.setAccessible(true); + inputField.set(copy, ""); + } + var causeField = Throwable.class.getDeclaredField("cause"); causeField.setAccessible(true); causeField.set(copy, adjustCopy(throwable.getCause(), true)); @@ -147,7 +109,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 -> { @@ -172,7 +134,11 @@ public class SentryErrorHandler implements ErrorHandler { } s.setTag("hasLicense", String.valueOf(LicenseProvider.get().hasPaidLicense())); - s.setTag("updatesEnabled", AppPrefs.get() != null ? AppPrefs.get().automaticallyUpdate().getValue().toString() : "unknown"); + s.setTag( + "updatesEnabled", + AppPrefs.get() != null + ? AppPrefs.get().automaticallyUpdate().getValue().toString() + : "unknown"); s.setTag("initError", String.valueOf(OperationMode.isInStartup())); s.setTag( "developerMode", @@ -182,12 +148,16 @@ public class SentryErrorHandler implements ErrorHandler { s.setTag("terminal", Boolean.toString(ee.isTerminal())); s.setTag("omitted", Boolean.toString(ee.isOmitted())); s.setTag("diagnostics", Boolean.toString(ee.isShouldSendDiagnostics())); - s.setTag("logs", Boolean.toString(ee.isShouldSendDiagnostics() && !ee.getAttachments().isEmpty())); + s.setTag( + "logs", + Boolean.toString( + ee.isShouldSendDiagnostics() && !ee.getAttachments().isEmpty())); s.setTag("inShutdown", Boolean.toString(OperationMode.isInShutdown())); + s.setTag("unhandled", Boolean.toString(ee.isUnhandled())); var exMessage = ee.getThrowable() != null ? ee.getThrowable().getMessage() : null; if (ee.getDescription() != null && !ee.getDescription().equals(exMessage) && ee.isShouldSendDiagnostics()) { - s.setTag("message", ee.getDescription().lines().collect(Collectors.joining(" "))); + s.setTag("message", ee.getDescription().lines().collect(Collectors.joining(" "))); } var user = new User(); @@ -198,4 +168,51 @@ public class SentryErrorHandler implements ErrorHandler { } s.setUser(user); } + + public void handle(ErrorEvent ee) { + // Assume that this object is wrapped by a synchronous error handler + if (!init) { + AppProperties.init(); + AppState.init(); + if (AppProperties.get().getSentryUrl() != null) { + Sentry.init(options -> { + options.setDsn(AppProperties.get().getSentryUrl()); + options.setEnableUncaughtExceptionHandler(false); + options.setAttachServerName(false); + options.setRelease(AppProperties.get().getVersion()); + options.setEnableShutdownHook(false); + options.setProguardUuid(AppProperties.get().getBuildUuid().toString()); + options.setTag("os", System.getProperty("os.name")); + options.setTag("osVersion", System.getProperty("os.version")); + options.setTag("arch", System.getProperty("os.arch")); + options.setDist(XPipeDistributionType.get().getId()); + options.setTag("staging", String.valueOf(AppProperties.get().isStaging())); + options.setSendModules(false); + options.setAttachThreads(false); + options.setEnableDeduplication(false); + options.setCacheDirPath( + AppProperties.get().getDataDir().resolve("cache").toString()); + }); + } + init = true; + } + + var id = captureEvent(ee); + if (id == null) { + return; + } + + var email = ee.getEmail(); + var hasEmail = email != null && !email.isBlank(); + var text = ee.getUserReport(); + if (hasUserReport(ee)) { + var fb = new UserFeedback(id); + if (hasEmail) { + fb.setEmail(email); + } + fb.setComments(text); + Sentry.captureUserFeedback(fb); + } + Sentry.flush(3000); + } } 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 f751db70..78f2649e 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 75bfaac2..29f5f1ca 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 e3b32b56..88ca112f 100644 --- a/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java +++ b/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java @@ -28,10 +28,12 @@ import java.util.concurrent.Callable; @CommandLine.Command( header = "Launches the XPipe daemon.", sortOptions = false, - showEndOfOptionsDelimiterInUsageHelp = true -) + showEndOfOptionsDelimiterInUsageHelp = true) public class LauncherCommand implements Callable { + @CommandLine.Parameters(paramLabel = "") + final List inputs = List.of(); + @CommandLine.Option( names = {"--mode"}, description = "The mode to launch the daemon in or switch too", @@ -39,11 +41,11 @@ public class LauncherCommand implements Callable { converter = LauncherModeConverter.class) XPipeDaemonMode mode; - @CommandLine.Parameters(paramLabel = "") - 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) -> { @@ -81,14 +83,19 @@ public class LauncherCommand implements Callable { if (BeaconServer.isReachable()) { try (var con = new LauncherConnection()) { con.constructSocket(); - con.performSimpleExchange(FocusExchange.Request.builder().mode(getEffectiveMode()).build()); + con.performSimpleExchange(FocusExchange.Request.builder() + .mode(getEffectiveMode()) + .build()); if (!inputs.isEmpty()) { - con.performSimpleExchange(OpenExchange.Request.builder().arguments(inputs).build()); + con.performSimpleExchange( + OpenExchange.Request.builder().arguments(inputs).build()); } if (OsType.getLocal().equals(OsType.MACOS)) { Desktop.getDesktop().setOpenURIHandler(e -> { - con.performSimpleExchange(OpenExchange.Request.builder().arguments(List.of(e.getURI().toString())).build()); + con.performSimpleExchange(OpenExchange.Request.builder() + .arguments(List.of(e.getURI().toString())) + .build()); }); ThreadHelper.sleep(1000); } @@ -101,12 +108,17 @@ public class LauncherCommand implements Callable { // there might be another instance running, for example // starting up or listening on another port if (!AppDataLock.lock()) { - throw new IOException("Data directory " + AppProperties.get().getDataDir().toString() + " is already locked"); + throw new IOException( + "Data directory " + AppProperties.get().getDataDir().toString() + " is already locked"); } } catch (Exception ex) { var cli = XPipeInstallation.getLocalDefaultCliExecutable(); - ErrorEvent.fromThrowable(ex).term().description("Unable to connect to existing running daemon instance as it did not respond." + - " Either try to kill the process xpiped manually or use the command \"" + cli + "\" daemon stop --force.").handle(); + ErrorEvent.fromThrowable(ex) + .term() + .description("Unable to connect to existing running daemon instance as it did not respond." + + " Either try to kill the process xpiped manually or use the command \"" + cli + + "\" daemon stop --force.") + .handle(); } } @@ -122,7 +134,9 @@ public class LauncherCommand implements Callable { return XPipeDaemonMode.get(opModeName); } - return AppPrefs.get() != null ? AppPrefs.get().startupBehaviour().getValue().getMode() : XPipeDaemonMode.GUI; + return AppPrefs.get() != null + ? AppPrefs.get().startupBehaviour().getValue().getMode() + : XPipeDaemonMode.GUI; } @Override @@ -139,14 +153,6 @@ public class LauncherCommand implements Callable { } LauncherInput.handle(inputs); - - // URL open operations have to be handled in a special way on macOS! - if (OsType.getLocal().equals(OsType.MACOS)) { - Desktop.getDesktop().setOpenURIHandler(e -> { - LauncherInput.handle(List.of(e.getURI().toString())); - }); - } - return 0; } } 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 7fb8b2dd..7ac8f0bb 100644 --- a/app/src/main/java/io/xpipe/app/launcher/LauncherInput.java +++ b/app/src/main/java/io/xpipe/app/launcher/LauncherInput.java @@ -24,9 +24,7 @@ public abstract class LauncherInput { return; } - TrackEvent.withDebug("launcher", "Handling arguments") - .elements(arguments) - .handle(); + TrackEvent.withDebug("Handling arguments").elements(arguments).handle(); var all = new ArrayList(); arguments.forEach(s -> { @@ -104,6 +102,11 @@ public abstract class LauncherInput { Path file; + @Override + public boolean requiresJavaFXPlatform() { + return true; + } + @Override public void execute() { if (!Files.exists(file)) { @@ -116,12 +119,7 @@ public abstract class LauncherInput { var dir = Files.isDirectory(file) ? file : file.getParent(); AppLayoutModel.get().selectBrowser(); - BrowserModel.DEFAULT.openFileSystemAsync( DataStorage.get().local().ref(), model -> dir.toString(), null); - } - - @Override - public boolean requiresJavaFXPlatform() { - return true; + BrowserModel.DEFAULT.openFileSystemAsync(DataStorage.get().local().ref(), model -> dir.toString(), null); } } diff --git a/app/src/main/java/io/xpipe/app/prefs/AboutCategory.java b/app/src/main/java/io/xpipe/app/prefs/AboutCategory.java new file mode 100644 index 00000000..57e95b0e --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/AboutCategory.java @@ -0,0 +1,125 @@ +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.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; +import javafx.scene.layout.Region; + +import java.util.List; + +public class AboutCategory extends AppPrefsCategory { + + private Comp createLinks() { + return new OptionsBuilder() + .addComp( + new TileButtonComp("discord", "discordDescription", "mdi2d-discord", e -> { + Hyperlinks.open(Hyperlinks.DISCORD); + e.consume(); + }) + .grow(true, false), + null) + .addComp( + new TileButtonComp("slack", "slackDescription", "mdi2s-slack", e -> { + Hyperlinks.open(Hyperlinks.SLACK); + e.consume(); + }) + .grow(true, false), + null) + .addComp( + new TileButtonComp("securityPolicy", "securityPolicyDescription", "mdrmz-security", e -> { + Hyperlinks.open(Hyperlinks.SECURITY); + e.consume(); + }) + .grow(true, false), + null) + .addComp( + new TileButtonComp("privacy", "privacyDescription", "mdomz-privacy_tip", e -> { + Hyperlinks.open(Hyperlinks.PRIVACY); + e.consume(); + }) + .grow(true, false), + null) + .addComp( + new TileButtonComp("thirdParty", "thirdPartyDescription", "mdi2o-open-source-initiative", e -> { + AppWindowHelper.sideWindow( + AppI18n.get("openSourceNotices"), + stage -> Comp.of(() -> createThirdPartyDeps()), + true, + null) + .show(); + e.consume(); + }) + .grow(true, false), + null) + .addComp( + new TileButtonComp("eula", "eulaDescription", "mdi2c-card-text-outline", e -> { + Hyperlinks.open(Hyperlinks.EULA); + e.consume(); + }) + .grow(true, false), + null) + .buildComp(); + } + + private Region createThirdPartyDeps() { + var list = new ThirdPartyDependencyListComp().createRegion(); + list.getStyleClass().add("open-source-notices"); + var sp = new ScrollPane(list); + sp.setFitToWidth(true); + sp.setPrefWidth(600); + sp.setPrefHeight(500); + return sp; + } + + @Override + protected String getId() { + return "about"; + } + + @Override + protected Comp create() { + var props = createProperties().padding(new Insets(0, 0, 0, 15)); + var update = new UpdateCheckComp().grow(true, false); + 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)); + } + + 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"); + } +} 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 ca45226a..e11ed5b5 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,189 @@ 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.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 { + 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 List> mapping = new ArrayList<>(); + + final BooleanProperty dontAutomaticallyStartVmSshServer = + mapVaultSpecific(new SimpleBooleanProperty(false), "dontAutomaticallyStartVmSshServer", Boolean.class); + final BooleanProperty dontAcceptNewHostKeys = + mapVaultSpecific(new SimpleBooleanProperty(false), "dontAcceptNewHostKeys", Boolean.class); + final BooleanProperty performanceMode = map(new SimpleBooleanProperty(false), "performanceMode", Boolean.class); + final BooleanProperty useBundledTools = map(new SimpleBooleanProperty(false), "useBundledTools", Boolean.class); + public final ObjectProperty theme = + map(new SimpleObjectProperty<>(), "theme", AppTheme.Theme.class); + final BooleanProperty useSystemFont = map(new SimpleBooleanProperty(true), "useSystemFont", Boolean.class); + final Property uiScale = map(new SimpleObjectProperty<>(null), "uiScale", Integer.class); + final BooleanProperty saveWindowLocation = + map(new SimpleBooleanProperty(true), "saveWindowLocation", Boolean.class); + final ObjectProperty terminalType = + map(new SimpleObjectProperty<>(), "terminalType", ExternalTerminalType.class); + final DoubleProperty windowOpacity = map(new SimpleDoubleProperty(1.0), "windowOpacity", Double.class); + final StringProperty customTerminalCommand = + map(new SimpleStringProperty(""), "customTerminalCommand", String.class); + final BooleanProperty preferTerminalTabs = + map(new SimpleBooleanProperty(true), "preferTerminalTabs", Boolean.class); + final BooleanProperty clearTerminalOnInit = + map(new SimpleBooleanProperty(true), "clearTerminalOnInit", Boolean.class); + public final BooleanProperty disableCertutilUse = + map(new SimpleBooleanProperty(false), "disableCertutilUse", Boolean.class); + public final BooleanProperty useLocalFallbackShell = + map(new SimpleBooleanProperty(false), "useLocalFallbackShell", Boolean.class); + public final BooleanProperty disableTerminalRemotePasswordPreparation = + mapVaultSpecific(new SimpleBooleanProperty(false), "disableTerminalRemotePasswordPreparation", Boolean.class); + public final Property alwaysConfirmElevation = + mapVaultSpecific(new SimpleObjectProperty<>(false), "alwaysConfirmElevation", Boolean.class); + public final BooleanProperty dontCachePasswords = + mapVaultSpecific(new SimpleBooleanProperty(false), "dontCachePasswords", Boolean.class); + public final BooleanProperty denyTempScriptCreation = + mapVaultSpecific(new SimpleBooleanProperty(false), "denyTempScriptCreation", Boolean.class); + final StringProperty passwordManagerCommand = + map(new SimpleStringProperty(""), "passwordManagerCommand", String.class); + final ObjectProperty startupBehaviour = + map(new SimpleObjectProperty<>(StartupBehaviour.GUI), "startupBehaviour", StartupBehaviour.class); + public final BooleanProperty enableGitStorage = + map(new SimpleBooleanProperty(false), "enableGitStorage", Boolean.class); + final StringProperty storageGitRemote = map(new SimpleStringProperty(""), "storageGitRemote", String.class); + final ObjectProperty closeBehaviour = + map(new SimpleObjectProperty<>(CloseBehaviour.QUIT), "closeBehaviour", CloseBehaviour.class); + final ObjectProperty externalEditor = + map(new SimpleObjectProperty<>(), "externalEditor", ExternalEditorType.class); + final StringProperty customEditorCommand = map(new SimpleStringProperty(""), "customEditorCommand", String.class); + final BooleanProperty preferEditorTabs = map(new SimpleBooleanProperty(true), "preferEditorTabs", Boolean.class); + final BooleanProperty automaticallyCheckForUpdates = + map(new SimpleBooleanProperty(true), "automaticallyCheckForUpdates", Boolean.class); + final BooleanProperty encryptAllVaultData = + mapVaultSpecific(new SimpleBooleanProperty(false), "encryptAllVaultData", Boolean.class); + final BooleanProperty enforceWindowModality = + map(new SimpleBooleanProperty(false), "enforceWindowModality", Boolean.class); + final BooleanProperty condenseConnectionDisplay = + map(new SimpleBooleanProperty(false), "condenseConnectionDisplay", Boolean.class); + final BooleanProperty showChildCategoriesInParentCategory = + map(new SimpleBooleanProperty(true), "showChildrenConnectionsInParentCategory", Boolean.class); + final BooleanProperty openConnectionSearchWindowOnConnectionCreation = + map(new SimpleBooleanProperty(true), "openConnectionSearchWindowOnConnectionCreation", Boolean.class); + final ObjectProperty storageDirectory = + map(new SimpleObjectProperty<>(DEFAULT_STORAGE_DIR), "storageDirectory", Path.class); + private final AppPrefsStorageHandler vaultStorageHandler = + new AppPrefsStorageHandler(storageDirectory().getValue().resolve("preferences.json")); + final BooleanProperty developerMode = map(new SimpleBooleanProperty(false), "developerMode", Boolean.class); + final BooleanProperty developerDisableUpdateVersionCheck = + map(new SimpleBooleanProperty(false), "developerDisableUpdateVersionCheck", Boolean.class); + private final ObservableBooleanValue developerDisableUpdateVersionCheckEffective = + bindDeveloperTrue(developerDisableUpdateVersionCheck); + final BooleanProperty developerDisableGuiRestrictions = + map(new SimpleBooleanProperty(false), "developerDisableGuiRestrictions", Boolean.class); + private final ObservableBooleanValue developerDisableGuiRestrictionsEffective = + bindDeveloperTrue(developerDisableGuiRestrictions); + private final ObjectProperty language = + map(new SimpleObjectProperty<>(SupportedLocale.ENGLISH), "language", SupportedLocale.class); + + @Getter + private final Property lockPassword = new SimpleObjectProperty<>(); + + @Getter + private final StringProperty lockCrypt = + mapVaultSpecific(new SimpleStringProperty(), "workspaceLock", String.class); + + private final IntegerProperty editorReloadTimeout = + map(new SimpleIntegerProperty(1000), "editorReloadTimeout", Integer.class); + private final BooleanProperty confirmDeletions = + map(new SimpleBooleanProperty(true), "confirmDeletions", Boolean.class); + + @Getter + private final List categories; + + private final AppPrefsStorageHandler globalStorageHandler = new AppPrefsStorageHandler( + AppProperties.get().getDataDir().resolve("settings").resolve("preferences.json")); + private final Map, Comp> customEntries = new LinkedHashMap<>(); + + @Getter + private final Property selectedCategory; + + private final PrefsHandler extensionHandler = new PrefsHandlerImpl(); + + private AppPrefs() { + this.categories = List.of( + new AboutCategory(), + new SystemCategory(), + new AppearanceCategory(), + new SyncCategory(), + new VaultCategory(), + new TerminalCategory(), + new EditorCategory(), + new LocalShellCategory(), + new SecurityCategory(), + new PasswordManagerCategory(), + new TroubleshootCategory(), + new DeveloperCategory()); + var selected = AppCache.get("selectedPrefsCategory", Integer.class, () -> 0); + if (selected == null) { + selected = 0; + } + this.selectedCategory = new SimpleObjectProperty<>( + categories.get(selected >= 0 && selected < categories.size() ? selected : 0)); + } + + public static void initLocal() { + INSTANCE = new AppPrefs(); + PrefsProvider.getAll().forEach(prov -> prov.addPrefs(INSTANCE.extensionHandler)); + INSTANCE.loadLocal(); + } + + public static void initSharedRemote() { + INSTANCE.loadSharedRemote(); + 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() { + INSTANCE.save(); + + // Keep instance as we might need some values on shutdown, e.g. on update with terminals + // INSTANCE = null; + } + + public static AppPrefs get() { + return INSTANCE; + } + public boolean isDevelopmentEnvironment() { return developerMode().getValue() && !ModuleHelper.isImage(); } @@ -62,218 +206,89 @@ 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; - 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<>(); + public ObservableValue language() { + return language; + } - // Languages - // ========= + public ObservableBooleanValue dontAutomaticallyStartVmSshServer() { + return dontAutomaticallyStartVmSshServer; + } - 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<>()); - - - - final BooleanProperty performanceMode = typed(new SimpleBooleanProperty(false), Boolean.class); + public ObservableBooleanValue dontAcceptNewHostKeys() { + return dontAcceptNewHostKeys; + } 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); - - public ReadOnlyIntegerProperty connectionTimeout() { - return connectionTimeOut; + public ObservableBooleanValue useBundledTools() { + return useBundledTools; } - private final BooleanProperty saveWindowLocation = typed(new SimpleBooleanProperty(true), 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<>()); - - // Lock - // ==== - - @Getter - private final Property lockPassword = new SimpleObjectProperty<>(); - @Getter - private final StringProperty lockCrypt = typed(new SimpleStringProperty(""), 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; - }); - - - // 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)); - - private final BooleanProperty preferTerminalTabs = typed(new SimpleBooleanProperty(true), Boolean.class); - private final BooleanField preferTerminalTabsField = - BooleanField.ofBooleanType(preferTerminalTabs).render(() -> new CustomToggleControl()); - - - // Fast terminal - // =========== - public final BooleanProperty enableFastTerminalStartup = typed(new SimpleBooleanProperty(false), Boolean.class); - public ObservableBooleanValue enableFastTerminalStartup() { - return enableFastTerminalStartup; + public ObservableValue useSystemFont() { + return useSystemFont; } - private final BooleanField enableFastTerminalStartupField = - BooleanField.ofBooleanType(enableFastTerminalStartup).render(() -> new CustomToggleControl()); - // Password manager - // ================ - final StringProperty passwordManagerCommand = typed(new SimpleStringProperty(""), String.class); + public ReadOnlyProperty uiScale() { + return uiScale; + } - // 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); + public ReadOnlyBooleanProperty clearTerminalOnInit() { + return clearTerminalOnInit; + } - private final SingleSelectionField startupBehaviourControl = Field.ofSingleSelectionType( - startupBehaviourList, startupBehaviour) - .render(() -> new TranslatableComboBoxControl<>()); + public ObservableBooleanValue disableCertutilUse() { + return disableCertutilUse; + } + + public ObservableBooleanValue useLocalFallbackShell() { + return useLocalFallbackShell; + } + + public ObservableBooleanValue disableTerminalRemotePasswordPreparation() { + return disableTerminalRemotePasswordPreparation; + } + + public ObservableValue alwaysConfirmElevation() { + return alwaysConfirmElevation; + } + + public ObservableBooleanValue dontCachePasswords() { + return dontCachePasswords; + } + + public ObservableBooleanValue denyTempScriptCreation() { + return denyTempScriptCreation; + } - // Git storage - // =========== - public final BooleanProperty enableGitStorage = typed(new SimpleBooleanProperty(false), Boolean.class); public ObservableBooleanValue enableGitStorage() { return enableGitStorage; } - final StringProperty storageGitRemote = typed(new SimpleStringProperty(""), 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<>()); + public ObservableBooleanValue encryptAllVaultData() { + return encryptAllVaultData; + } - // External editor - // =============== - final ObjectProperty externalEditor = - typed(new SimpleObjectProperty<>(), ExternalEditorType.class); - private final SingleSelectionField externalEditorControl = Field.ofSingleSelectionType( - externalEditorList, externalEditor) - .render(() -> new TranslatableComboBoxControl<>()); + public ObservableBooleanValue enforceWindowModality() { + return enforceWindowModality; + } - 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); + public ObservableBooleanValue condenseConnectionDisplay() { + return condenseConnectionDisplay; + } - private final BooleanProperty preferEditorTabs = typed(new SimpleBooleanProperty(true), Boolean.class); - private final BooleanField preferEditorTabsField = - BooleanField.ofBooleanType(preferEditorTabs).render(() -> new CustomToggleControl()); + public ObservableBooleanValue showChildCategoriesInParentCategory() { + return showChildCategoriesInParentCategory; + } - // Automatically update - // ==================== - private final BooleanProperty automaticallyCheckForUpdates = typed(new SimpleBooleanProperty(true), Boolean.class); - private final BooleanField automaticallyCheckForUpdatesField = - BooleanField.ofBooleanType(automaticallyCheckForUpdates).render(() -> new CustomToggleControl()); - - private final BooleanProperty confirmDeletions = typed(new SimpleBooleanProperty(true), Boolean.class); - - // Storage - // ======= - final ObjectProperty storageDirectory = - typed(new SimpleObjectProperty<>(DEFAULT_STORAGE_DIR), Path.class); - final StringField storageDirectoryControl = - PrefFields.ofPath(storageDirectory).validate(CustomValidators.absolutePath(), CustomValidators.directory()); - - // 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 developerDisableUpdateVersionCheck = - typed(new SimpleBooleanProperty(false), Boolean.class); - final BooleanField developerDisableUpdateVersionCheckField = - BooleanField.ofBooleanType(developerDisableUpdateVersionCheck).render(() -> new CustomToggleControl()); - private final ObservableBooleanValue developerDisableUpdateVersionCheckEffective = - bindDeveloperTrue(developerDisableUpdateVersionCheck); - - final BooleanProperty developerDisableGuiRestrictions = - typed(new SimpleBooleanProperty(false), Boolean.class); - final BooleanField developerDisableGuiRestrictionsField = - BooleanField.ofBooleanType(developerDisableGuiRestrictions).render(() -> new CustomToggleControl()); - 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 ObservableBooleanValue openConnectionSearchWindowOnConnectionCreation() { + return openConnectionSearchWindowOnConnectionCreation; + } public ReadOnlyProperty closeBehaviour() { return closeBehaviour; @@ -287,21 +302,30 @@ 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 +362,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,169 +383,90 @@ public class AppPrefs { return developerDisableGuiRestrictionsEffective; } - public ObservableBooleanValue developerDisableConnectorInstallationVersionCheck() { - return developerDisableConnectorInstallationVersionCheckEffective; + @SuppressWarnings("unchecked") + private T map(T o, String name, Class clazz) { + mapping.add(new Mapping<>(name, (Property) o, (Class) clazz)); + return o; } - public ObservableBooleanValue developerShowHiddenProviders() { - return developerShowHiddenProvidersEffective; + @SuppressWarnings("unchecked") + private T mapVaultSpecific(T o, String name, Class clazz) { + mapping.add(new Mapping<>(name, (Property) o, (Class) clazz, true)); + return o; } - public ObservableBooleanValue developerShowHiddenEntries() { - return developerShowHiddenEntriesEffective; - } - - private AppPreferencesFx preferencesFx; - private boolean controlsSetup; - - @Getter - private final Set> proRequiredSettings = new HashSet<>(); - - private AppPrefs() { - try { - preferencesFx = createPreferences(); - } catch (Exception e) { - ErrorEvent.fromThrowable(e).terminal(true).build().handle(); - } - - SimpleChangeListener.apply(languageInternal, val -> { - language.setValue(val); + public void setFromExternal(ObservableValue prop, T newValue) { + var writable = (Property) prop; + PlatformThread.runLaterIfNeededBlocking(() -> { + writable.setValue(newValue); + save(); }); } - public static void init() { - INSTANCE = new AppPrefs(); - INSTANCE.preferencesFx.loadSettings(); - INSTANCE.initValues(); - PrefsProvider.getAll().forEach(prov -> prov.init()); - } - - public static void reset() { - INSTANCE.save(); - - // Keep instance as we might need some values on shutdown, e.g. on update with terminals - // INSTANCE = null; - } - - public static AppPrefs get() { - return INSTANCE; - } - - // Storage directory - // ================= - - private T typed(T o, Class clazz) { - classMap.put(o, clazz); - return o; - } - - private > T editable(T o, ObservableBooleanValue v) { - o.editableProperty().bind(v); - 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) { - var writable = (Property) prop; - 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(); + } + + private void loadLocal() { + for (Mapping value : mapping) { + if (value.isVaultSpecific()) { + continue; + } + + loadValue(globalStorageHandler, value); + } + } + + private void loadSharedRemote() { + for (Mapping value : mapping) { + if (!value.isVaultSpecific()) { + continue; + } + + var def = value.getProperty().getValue(); + var r = loadValue(vaultStorageHandler, value); + + // This can be used to facilitate backwards compatibility + var isDefault = Objects.equals(r, def); + if (isDefault) { + 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 +480,37 @@ 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); + @Value + public static class Mapping { - 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)))); + String key; + Property property; + Class valueClass; + boolean vaultSpecific; - categories.get(categories.size() - 2).setVisibilityProperty(VisibilityProperty.of(developerMode())); + public Mapping(String key, Property property, Class valueClass) { + this.key = key; + this.property = property; + this.valueClass = valueClass; + this.vaultSpecific = false; + } - 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)); + public Mapping(String key, Property property, Class valueClass, boolean vaultSpecific) { + this.key = key; + this.property = property; + this.valueClass = valueClass; + this.vaultSpecific = vaultSpecific; + } } @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 524768ff..03696a99 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 00000000..38caefcd --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java @@ -0,0 +1,57 @@ +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 00000000..43de8d9a --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefsSidebarComp.java @@ -0,0 +1,50 @@ +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 00000000..903bc17a --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefsStorageHandler.java @@ -0,0 +1,100 @@ +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.withDebug("Preferences value not found").tag("id", id).tag("default", defaultObject).handle(); + 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 00000000..2b85895c --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/AppearanceCategory.java @@ -0,0 +1,59 @@ +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) + .nameAndDescription("showChildCategoriesInParentCategory") + .addToggle(prefs.showChildCategoriesInParentCategory)) + .addTitle("workflow") + .sub(new OptionsBuilder() + .nameAndDescription("openConnectionSearchWindowOnConnectionCreation") + .addToggle(prefs.openConnectionSearchWindowOnConnectionCreation)) + .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/CloseBehaviour.java b/app/src/main/java/io/xpipe/app/prefs/CloseBehaviour.java index c876b30e..325c21ed 100644 --- a/app/src/main/java/io/xpipe/app/prefs/CloseBehaviour.java +++ b/app/src/main/java/io/xpipe/app/prefs/CloseBehaviour.java @@ -11,7 +11,6 @@ public enum CloseBehaviour implements PrefsChoiceValue { public void run() { OperationMode.shutdown(false, false); } - }, MINIMIZE_TO_TRAY("app.minimizeToTray") { 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 a5d16375..645cc107 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/DeveloperCategory.java b/app/src/main/java/io/xpipe/app/prefs/DeveloperCategory.java index 69f795d2..08fed833 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,44 +44,26 @@ public class DeveloperCategory extends AppPrefsCategory { }); }; - var runLocalCommand = lazyNode( - "shellCommandTest", - new HorizontalComp(List.of( - new TextFieldComp(localCommand) - .apply(struc -> struc.get().setPromptText("Local command")) - .styleClass(Styles.LEFT_PILL) - .grow(false, true), - new ButtonComp(null, new FontIcon("mdi2p-play"), test) - .styleClass(Styles.RIGHT_PILL) - .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) - ); + var runLocalCommand = new HorizontalComp(List.of( + new TextFieldComp(localCommand) + .apply(struc -> struc.get().setPromptText("Local command")) + .styleClass(Styles.LEFT_PILL) + .grow(false, true), + new ButtonComp(null, new FontIcon("mdi2p-play"), test) + .styleClass(Styles.RIGHT_PILL) + .grow(false, true))) + .padding(new Insets(15, 0, 0, 0)) + .apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)) + .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 00000000..caefd19e --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/EditorCategory.java @@ -0,0 +1,56 @@ +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 a5155b6d..87bac1c6 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; @@ -20,15 +23,15 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue { this.id = id; } + public abstract boolean isAvailable(); + + public abstract boolean isSelectable(); + @Override public String getId() { return id; } - public abstract boolean isSelectable(); - - public abstract boolean isAvailable(); - @Override public String toString() { return getId(); @@ -57,16 +60,21 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue { // Check if returned paths are actually valid // Also sort them by length to prevent finding a deeply buried app - var valid = path.lines().filter(s -> { - try { - return Files.exists(Path.of(s)); - } catch (Exception ex) { - return false; - } - }).sorted(Comparator.comparingInt(value -> value.length())).toList(); + var valid = path.lines() + .filter(s -> { + try { + return Files.exists(Path.of(s)); + } catch (Exception ex) { + return false; + } + }) + .sorted(Comparator.comparingInt(value -> value.length())) + .toList(); // Require app in proper applications directory - var app = valid.stream().filter(s -> s.contains("Applications")).findFirst(); + var app = valid.stream() + .filter(s -> s.contains("Applications")) + .findFirst(); return app.map(Path::of); } } catch (Exception e) { @@ -76,13 +84,13 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue { } @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.MACOS); + public boolean isAvailable() { + return getApplicationPath().isPresent(); } @Override - public boolean isAvailable() { - return getApplicationPath().isPresent(); + public boolean isSelectable() { + return OsType.getLocal().equals(OsType.MACOS); } } @@ -103,6 +111,35 @@ 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 +156,8 @@ 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(); @@ -135,11 +173,6 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue { return Optional.empty(); } - @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.WINDOWS); - } - @Override public boolean isAvailable() { var path = determineFromPath(); @@ -150,5 +183,10 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue { var installation = determineInstallation(); return installation.isPresent() && Files.exists(installation.get()); } + + @Override + public boolean isSelectable() { + return OsType.getLocal().equals(OsType.WINDOWS); + } } } 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 8ae784ee..6b154c9d 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,31 +46,21 @@ 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() { return Optional.of(Path.of(System.getenv("LOCALAPPDATA")) - .resolve("Programs") - .resolve("Microsoft VS Code Insiders") - .resolve("bin") - .resolve("code-insiders.cmd")); - } - - @Override - public boolean detach() { - return false; + .resolve("Programs") + .resolve("Microsoft VS Code Insiders") + .resolve("bin") + .resolve("code-insiders.cmd")); } }; - ExternalEditorType NOTEPADPLUSPLUS_WINDOWS = new WindowsType("app.notepad++", "notepad++") { + ExternalEditorType NOTEPADPLUSPLUS = new WindowsType("app.notepad++", "notepad++", false) { @Override protected Optional determineInstallation() { @@ -84,7 +68,8 @@ public interface ExternalEditorType extends PrefsChoiceValue { // Check 32 bit install if (found.isEmpty()) { - found = WindowsRegistry.readString(WindowsRegistry.HKEY_LOCAL_MACHINE, "WOW6432Node\\SOFTWARE\\Notepad++", null); + found = WindowsRegistry.readString( + WindowsRegistry.HKEY_LOCAL_MACHINE, "WOW6432Node\\SOFTWARE\\Notepad++", null); } return found.map(p -> p + "\\notepad++.exe").map(Path::of); } @@ -105,41 +90,11 @@ public interface ExternalEditorType extends PrefsChoiceValue { LinuxPathType MOUSEPAD = new LinuxPathType("app.mousepad", "mousepad"); LinuxPathType PLUMA = new LinuxPathType("app.pluma", "pluma"); - - class MacOsEditor extends ExternalApplicationType.MacApplication implements ExternalEditorType { - - public MacOsEditor(String id, String applicationName) { - super(id, applicationName); - } - - @Override - public void launch(Path file) throws Exception { - var execFile = getApplicationPath(); - if (execFile.isEmpty()) { - throw new IOException("Application " + applicationName + ".app not found"); - } - - ApplicationHelper.executeLocalApplication( - shellControl -> String.format( - "open -a %s %s", - shellControl - .getShellDialect() - .fileArgument(execFile.orElseThrow().toString()), - shellControl.getShellDialect().fileArgument(file.toString())), - false); - } - } - ExternalEditorType TEXT_EDIT = new MacOsEditor("app.textEdit", "TextEdit"); - ExternalEditorType BBEDIT = new MacOsEditor("app.bbedit", "BBEdit"); - ExternalEditorType SUBLIME_MACOS = new MacOsEditor("app.sublime", "Sublime Text"); - ExternalEditorType VSCODE_MACOS = new MacOsEditor("app.vscode", "Visual Studio Code"); - ExternalEditorType VSCODIUM_MACOS = new MacOsEditor("app.vscodium", "VSCodium"); - ExternalEditorType CUSTOM = new ExternalEditorType() { @Override @@ -149,8 +104,11 @@ public interface ExternalEditorType extends PrefsChoiceValue { throw ErrorEvent.unreportable(new IllegalStateException("No custom editor command specified")); } - var format = customCommand.toLowerCase(Locale.ROOT).contains("$file") ? customCommand : customCommand + " $FILE"; - ApplicationHelper.executeLocalApplication(sc -> ApplicationHelper.replaceFileArgument(format, "FILE", file.toString()), true); + var format = + customCommand.toLowerCase(Locale.ROOT).contains("$file") ? customCommand : customCommand + " $FILE"; + ApplicationHelper.executeLocalApplication( + CommandBuilder.of().add(ApplicationHelper.replaceFileArgument(format, "FILE", file.toString())), + true); } @Override @@ -158,80 +116,15 @@ public interface ExternalEditorType extends PrefsChoiceValue { return "app.custom"; } }; - - void launch(Path file) throws Exception; - - class GenericPathType extends ExternalApplicationType.PathApplication implements ExternalEditorType { - - public GenericPathType(String id, String command) { - super(id, command); - } - - @Override - public void launch(Path file) throws Exception { - LocalShell.getShell().executeSimpleCommand(CommandBuilder.of().add(executable).addFile(file.toString())); - } - - @Override - public boolean isSelectable() { - return true; - } - } - - class LinuxPathType extends GenericPathType { - - public LinuxPathType(String id, String command) { - super(id, command); - } - - @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.LINUX); - } - } - - abstract class WindowsType extends ExternalApplicationType.WindowsType - implements ExternalEditorType { - - private final String executable; - - public WindowsType(String id, String executable) { - super(id, executable); - this.executable = executable; - } - - public boolean detach() { - return true; - } - - @Override - public void launch(Path file) throws Exception { - var location = determineFromPath(); - if (location.isEmpty()) { - location = determineInstallation(); - if (location.isEmpty()) { - throw ErrorEvent.unreportable(new IOException("Unable to find installation of " + toTranslatedString())); - } - } - - Optional finalLocation = location; - ApplicationHelper.executeLocalApplication( - sc -> String.format( - "%s %s", - sc.getShellDialect().fileArgument(finalLocation.get().toString()), - sc.getShellDialect().fileArgument(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"); - - 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); + 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, 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 +151,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 +171,7 @@ public interface ExternalEditorType extends PrefsChoiceValue { } } else { typeProperty.set(LINUX_EDITORS.stream() - .filter(externalEditorType -> externalEditorType.isAvailable()) + .filter(ExternalApplicationType.PathApplication::isAvailable) .findFirst() .orElse(null)); } @@ -286,9 +179,92 @@ 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)); } } + + void launch(Path file) throws Exception; + + class MacOsEditor extends ExternalApplicationType.MacApplication implements ExternalEditorType { + + public MacOsEditor(String id, String applicationName) { + super(id, applicationName); + } + + @Override + public void launch(Path file) throws Exception { + var execFile = getApplicationPath(); + if (execFile.isEmpty()) { + throw new IOException("Application " + applicationName + ".app not found"); + } + + ApplicationHelper.executeLocalApplication( + CommandBuilder.of() + .add("open", "-a") + .addFile(execFile.orElseThrow().toString()) + .addFile(file.toString()), + false); + } + } + + class GenericPathType extends ExternalApplicationType.PathApplication implements ExternalEditorType { + + 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 { + ApplicationHelper.executeLocalApplication( + CommandBuilder.of().add(executable).addFile(file.toString()), detach); + } + + @Override + public boolean isSelectable() { + return true; + } + } + + class LinuxPathType extends GenericPathType { + + public LinuxPathType(String id, String command) { + super(id, command, true); + } + + @Override + public boolean isSelectable() { + return OsType.getLocal().equals(OsType.LINUX); + } + } + + abstract class WindowsType extends ExternalApplicationType.WindowsType implements ExternalEditorType { + + private final boolean detach; + + public WindowsType(String id, String executable, boolean detach) { + super(id, executable); + this.detach = detach; + } + + @Override + public void launch(Path file) throws Exception { + var location = determineFromPath(); + if (location.isEmpty()) { + location = determineInstallation(); + if (location.isEmpty()) { + throw ErrorEvent.unreportable( + new IOException("Unable to find installation of " + toTranslatedString())); + } + } + + Optional finalLocation = location; + ApplicationHelper.executeLocalApplication( + CommandBuilder.of().addFile(finalLocation.get().toString()).addFile(file.toString()), detach); + } + } } 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 44430963..c9712a9e 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java @@ -4,34 +4,25 @@ 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())); + public boolean supportsTabs() { + return false; } @Override @@ -40,12 +31,21 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.WINDOWS); + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + if (configuration.getScriptDialect().equals(ShellDialects.CMD)) { + return CommandBuilder.of().add("/c").add(configuration.getScriptFile()); + } + + return CommandBuilder.of().add("/c").add(configuration.getDialectLaunchCommand()); } }; - ExternalTerminalType POWERSHELL_WINDOWS = new SimplePathType("app.powershell", "powershell") { + ExternalTerminalType POWERSHELL = new SimplePathType("app.powershell", "powershell") { + + @Override + public boolean supportsTabs() { + return false; + } @Override public boolean supportsColoredTitle() { @@ -53,18 +53,24 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } @Override - protected CommandBuilder toCommand(String name, String file) { - return CommandBuilder.of() - .add("-ExecutionPolicy", "RemoteSigned", "-NoProfile", "-Command", "cmd", "/C", "'" + file + "'"); - } + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + if (configuration.getScriptDialect().equals(ShellDialects.POWERSHELL)) { + return CommandBuilder.of() + .add("-ExecutionPolicy", "Bypass") + .add("-File") + .add(configuration.getScriptFile()); + } - @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.WINDOWS); + return CommandBuilder.of().add("-Command").add(configuration.getDialectLaunchCommand()); } }; - ExternalTerminalType PWSH_WINDOWS = new SimplePathType("app.pwsh", "pwsh") { + ExternalTerminalType PWSH = new SimplePathType("app.pwsh", "pwsh") { + + @Override + public boolean supportsTabs() { + return false; + } @Override public boolean supportsColoredTitle() { @@ -72,31 +78,36 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } @Override - protected CommandBuilder toCommand(String name, String file) { + 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()) @@ -107,7 +118,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue { private Path getPath() { var local = System.getenv("LOCALAPPDATA"); - return Path.of(local).resolve("Microsoft\\WindowsApps\\Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe\\wt.exe"); + return Path.of(local) + .resolve("Microsoft\\WindowsApps\\Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe\\wt.exe"); } @Override @@ -115,39 +127,43 @@ 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") - .addQuoted(fixedName) - .addFile(configuration.getScriptFile())); - } - - @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.WINDOWS); + 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) + .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,58 +171,33 @@ 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()) - .add("-e") - .add("cmd") - .add("/c") - .addQuoted(configuration.getScriptFile().replaceAll(" ", "^$0"))); - } - - @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.WINDOWS); + return b.add("-t") + .addQuoted(configuration.getCleanTitle()) + .add("-e") + .add("cmd") + .add("/c") + .addQuoted(configuration.getScriptFile().replaceAll(" ", "^$0")); } }; - - abstract class WindowsType extends ExternalApplicationType.WindowsType implements ExternalTerminalType { - - public WindowsType(String id, String executable) { - super(id, executable); - } + ExternalTerminalType TABBY_WINDOWS = new WindowsType("app.tabby", "Tabby") { @Override - public void launch(LaunchConfiguration configuration) throws Exception { - var location = determineFromPath(); - if (location.isEmpty()) { - location = determineInstallation(); - if (location.isEmpty()) { - throw new IOException("Unable to find installation of " + toTranslatedString()); - } - } - - execute(location.get(), configuration); + public boolean supportsTabs() { + return true; } - protected abstract void execute(Path file, LaunchConfiguration configuration) throws Exception; - } - - ExternalTerminalType TABBY_WINDOWS = new WindowsType("app.tabbyWindows", "Tabby") { - @Override protected void execute(Path file, LaunchConfiguration configuration) throws Exception { - ApplicationHelper.executeLocalApplication( - shellControl -> shellControl.getShellDialect().fileArgument(file.toString()) + " run " - + shellControl.getShellDialect().fileArgument(configuration.getScriptFile()), - true); + // Tabby has a very weird handling of output, even detaching with start does not prevent it from printing + LocalShell.getShell().executeSimpleCommand( + CommandBuilder.of().addFile(file.toString()).add("run").addFile(configuration.getScriptFile()).discardOutput()); } @Override @@ -215,7 +206,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue { WindowsRegistry.HKEY_CURRENT_USER, "SOFTWARE\\71445fac-d6ef-5436-9da7-5a323762d7f5", "InstallLocation") - .map(p -> p + "\\Tabby.exe").map(Path::of); + .map(p -> p + "\\Tabby.exe") + .map(Path::of); if (perUser.isPresent()) { return perUser; } @@ -224,74 +216,64 @@ public interface ExternalTerminalType extends PrefsChoiceValue { WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\71445fac-d6ef-5436-9da7-5a323762d7f5", "InstallLocation") - .map(p -> p + "\\Tabby.exe").map(Path::of); + .map(p -> p + "\\Tabby.exe") + .map(Path::of); return systemWide; } }; - - ExternalTerminalType WEZ_WINDOWS = new WindowsType("app.wezWindows", "wezterm-gui") { - - @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(); - } - - @Override - protected Optional determineInstallation() { - Optional launcherDir; - launcherDir = WindowsRegistry.readString( - WindowsRegistry.HKEY_LOCAL_MACHINE, - "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{BCF6F0DA-5B9A-408D-8562-F680AE6E1EAF}_is1", - "InstallLocation") - .map(p -> p + "\\wezterm-gui.exe"); - return launcherDir.map(Path::of); - } - }; - - ExternalTerminalType WEZ_LINUX = new SimplePathType("app.wezLinux", "wezterm-gui") { + ExternalTerminalType WEZ_WINDOWS = new WindowsType("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 void execute(Path file, LaunchConfiguration configuration) throws Exception { + ApplicationHelper.executeLocalApplication( + CommandBuilder.of().addFile(file.toString()).add("start").addFile(configuration.getScriptFile()), + true); + } + + @Override + protected Optional determineInstallation() { + Optional launcherDir; + launcherDir = WindowsRegistry.readString( + WindowsRegistry.HKEY_LOCAL_MACHINE, + "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{BCF6F0DA-5B9A-408D-8562-F680AE6E1EAF}_is1", + "InstallLocation") + .map(p -> p + "\\wezterm-gui.exe"); + return launcherDir.map(Path::of); } }; + ExternalTerminalType WEZ_LINUX = new SimplePathType("app.wezterm", "wezterm-gui") { - // 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); - // } - // }; + @Override + public boolean supportsTabs() { + return false; + } - ExternalTerminalType GNOME_TERMINAL = new PathType("app.gnomeTerminal", "gnome-terminal") { + @Override + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + return CommandBuilder.of().add("start").addFile(configuration.getScriptFile()); + } + }; + ExternalTerminalType GNOME_TERMINAL = new PathCheckType("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,14 +283,13 @@ public interface ExternalTerminalType extends PrefsChoiceValue { pc.executeSimpleCommand(toExecute); } } + }; + ExternalTerminalType KONSOLE = new SimplePathType("app.konsole", "konsole") { @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.LINUX); + public boolean supportsTabs() { + return true; } - }; - - ExternalTerminalType KONSOLE = new SimplePathType("app.konsole", "konsole") { @Override public boolean supportsColoredTitle() { @@ -316,217 +297,328 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } @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) { + public boolean supportsTabs() { + return true; + } + + @Override + protected CommandBuilder toCommand(LaunchConfiguration configuration) { return CommandBuilder.of() .add("-e") - .addQuoted(file) + .addQuoted(configuration.getScriptFile()) .add("-T") - .addQuoted(name) + .addQuoted(configuration.getColoredTitle()) .add("--new-tab"); } + }; + ExternalTerminalType KITTY_LINUX = new SimplePathType("app.kitty", "kitty") { @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.LINUX); + public boolean supportsTabs() { + return false; + } + + @Override + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + return CommandBuilder.of() + .add("-1") + .add("-T") + .addQuoted(configuration.getColoredTitle()) + .addQuoted(configuration.getScriptFile()); } }; - - ExternalTerminalType KITTY = new SimplePathType("app.kitty", "kitty") { - - @Override - protected CommandBuilder toCommand(String name, String file) { - return CommandBuilder.of().add("-1").add("-T").addQuoted(name).addQuoted(file); - } - - @Override - public boolean isSelectable() { - return OsType.getLocal().equals(OsType.LINUX); - } - }; - 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()); } }; - - 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 MacOsType("app.macosTerminal", "Terminal") { + @Override + public boolean supportsTabs() { + return false; + } - ExternalTerminalType MACOS_TERMINAL = new MacOsTerminalType(); + @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 ITERM2 = new MacOsType("app.iterm2", "iTerm") { + @Override + public boolean supportsTabs() { + return true; + } - ExternalTerminalType ITERM2 = new ITerm2Type(); + @Override + public void launch(LaunchConfiguration configuration) throws Exception { + var app = this.getApplicationPath(); + if (app.isEmpty()) { + throw new IllegalStateException("iTerm installation not found"); + } - ExternalTerminalType WARP = new WarpType(); + 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; + } - ExternalTerminalType TABBY_MAC_OS = new TabbyMacOsType(); + @Override + public boolean shouldClear() { + return false; + } - ExternalTerminalType ALACRITTY_MACOS = new MacOsType("app.alacrittyMacOs", "Alacritty") { + @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,27 +632,40 @@ 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.wezterm", "WezTerm") { - ExternalTerminalType WEZ_MACOS = new MacOsType("app.wezMacOs", "WezTerm") { + @Override + public boolean supportsTabs() { + return false; + } @Override public void launch(LaunchConfiguration configuration) throws Exception { var c = CommandBuilder.of() - .addFile(getApplicationPath().orElseThrow().resolve("Contents").resolve("MacOS") - .resolve("wezterm-gui").toString()) - .add("start") - .addFile(configuration.getScriptFile()).buildString(LocalShell.getShell()); + .addFile(getApplicationPath() + .orElseThrow() + .resolve("Contents") + .resolve("MacOS") + .resolve("wezterm-gui") + .toString()) + .add("start") + .addFile(configuration.getScriptFile()) + .buildString(LocalShell.getShell()); c = ApplicationHelper.createDetachCommand(LocalShell.getShell(), c); LocalShell.getShell().executeSimpleCommand(c); } }; + ExternalTerminalType KITTY_MACOS = new MacOsType("app.kitty", "kitty") { - ExternalTerminalType KITTY_MACOS = new MacOsType("app.kittyMacOs", "kitty") { + @Override + public boolean supportsTabs() { + return false; + } @Override public boolean supportsColoredTitle() { @@ -596,46 +701,56 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } } }; - ExternalTerminalType CUSTOM = new CustomType(); + 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); - 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(); + @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 getDefault() { + static ExternalTerminalType determineDefault() { return ALL.stream() .filter(externalTerminalType -> !externalTerminalType.equals(CUSTOM)) .filter(terminalType -> terminalType.isAvailable()) @@ -643,14 +758,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { .orElse(null); } - @Value - class LaunchConfiguration { - - DataStoreColor color; - String title; - String cleanTitle; - String scriptFile; - } + boolean supportsTabs(); default boolean supportsColoredTitle() { return true; @@ -662,25 +770,48 @@ public interface ExternalTerminalType extends PrefsChoiceValue { default void launch(LaunchConfiguration configuration) throws Exception {} - class MacOsTerminalType extends ExternalApplicationType.MacApplication implements ExternalTerminalType { + abstract class WindowsType extends ExternalApplicationType.WindowsType implements ExternalTerminalType { - public MacOsTerminalType() { - super("app.macosTerminal", "Terminal"); + public WindowsType(String id, String executable) { + super(id, executable); } @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(); + var location = determineFromPath(); + if (location.isEmpty()) { + location = determineInstallation(); + if (location.isEmpty()) { + throw new IOException("Unable to find installation of " + toTranslatedString()); + } } + + execute(location.get(), configuration); + } + + protected abstract void execute(Path file, LaunchConfiguration configuration) throws Exception; + } + + @Value + class LaunchConfiguration { + DataStoreColor color; + 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; } } @@ -691,8 +822,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } @Override - public boolean supportsColoredTitle() { - return false; + public boolean supportsTabs() { + return true; } @Override @@ -707,7 +838,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"; } @@ -715,105 +846,14 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } } - @Override - public boolean isSelectable() { - return true; - } - @Override public boolean isAvailable() { return true; } - } - - 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(); - } + public boolean isSelectable() { + return true; } } @@ -825,21 +865,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 +878,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 +886,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/LocalShellCategory.java b/app/src/main/java/io/xpipe/app/prefs/LocalShellCategory.java new file mode 100644 index 00000000..b51696f6 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/LocalShellCategory.java @@ -0,0 +1,28 @@ +package io.xpipe.app.prefs; + +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.util.OptionsBuilder; +import io.xpipe.core.process.OsType; +import javafx.beans.property.SimpleBooleanProperty; + +public class LocalShellCategory extends AppPrefsCategory { + + @Override + protected String getId() { + return "localShell"; + } + + @Override + protected Comp create() { + var prefs = AppPrefs.get(); + return new OptionsBuilder() + .addTitle("localShell") + .sub(new OptionsBuilder() + .nameAndDescription("useBundledTools") + .addToggle(prefs.useBundledTools) + .hide(new SimpleBooleanProperty(!OsType.getLocal().equals(OsType.WINDOWS))) + .nameAndDescription("useLocalFallbackShell") + .addToggle(prefs.useLocalFallbackShell)) + .buildComp(); + } +} diff --git a/app/src/main/java/io/xpipe/app/prefs/PasswordManagerCategory.java b/app/src/main/java/io/xpipe/app/prefs/PasswordManagerCategory.java new file mode 100644 index 00000000..9015318d --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/PasswordManagerCategory.java @@ -0,0 +1,72 @@ +package io.xpipe.app.prefs; + +import atlantafx.base.theme.Styles; +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.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.store.LocalStore; +import javafx.beans.property.SimpleStringProperty; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.util.List; + +public class PasswordManagerCategory extends AppPrefsCategory { + + @Override + protected String getId() { + return "passwordManager"; + } + + @Override + protected Comp create() { + var prefs = AppPrefs.get(); + var testPasswordManagerValue = new SimpleStringProperty(); + Runnable test = () -> { + prefs.save(); + var cmd = prefs.passwordManagerString(testPasswordManagerValue.get()); + if (cmd == null) { + return; + } + + ThreadHelper.runFailableAsync(() -> { + TerminalLauncher.open( + "Password test", + new LocalStore() + .control() + .command(CommandBuilder.ofFunction(sc -> cmd + + "\n" + + sc.getShellDialect().getEchoCommand("Is this your password?", false))) + .terminalExitMode(CommandControl.TerminalExitMode.KEEP_OPEN)); + }); + }; + + var testPasswordManager = new HorizontalComp(List.of( + new TextFieldComp(testPasswordManagerValue) + .apply(struc -> struc.get().setPromptText("Enter password key")) + .styleClass(Styles.LEFT_PILL) + .grow(false, true), + new ButtonComp(null, new FontIcon("mdi2p-play"), test) + .styleClass(Styles.RIGHT_PILL) + .grow(false, true))) + .padding(new Insets(15, 0, 0, 0)) + .apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)) + .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/SecurityCategory.java b/app/src/main/java/io/xpipe/app/prefs/SecurityCategory.java new file mode 100644 index 00000000..c3b4f875 --- /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.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("alwaysConfirmElevation") + .addToggle(prefs.alwaysConfirmElevation) + .nameAndDescription("dontCachePasswords") + .addToggle(prefs.dontCachePasswords) + .nameAndDescription("denyTempScriptCreation") + .addToggle(prefs.denyTempScriptCreation) + .nameAndDescription("disableCertutilUse") + .addToggle(prefs.disableCertutilUse) + .nameAndDescription("dontAcceptNewHostKeys") + .addToggle(prefs.dontAcceptNewHostKeys) + .nameAndDescription("dontAutomaticallyStartVmSshServer") + .addToggle(prefs.dontAutomaticallyStartVmSshServer) + .nameAndDescription("disableTerminalRemotePasswordPreparation") + .addToggle(prefs.disableTerminalRemotePasswordPreparation)); + return builder.buildComp(); + } +} diff --git a/app/src/main/java/io/xpipe/app/prefs/StartupBehaviour.java b/app/src/main/java/io/xpipe/app/prefs/StartupBehaviour.java index 53460b83..48f2471b 100644 --- a/app/src/main/java/io/xpipe/app/prefs/StartupBehaviour.java +++ b/app/src/main/java/io/xpipe/app/prefs/StartupBehaviour.java @@ -9,8 +9,7 @@ import lombok.Getter; @Getter @AllArgsConstructor public enum StartupBehaviour implements PrefsChoiceValue { - GUI("app.startGui", XPipeDaemonMode.GUI) { - }, + GUI("app.startGui", XPipeDaemonMode.GUI) {}, TRAY("app.startInTray", XPipeDaemonMode.TRAY) { public boolean isSelectable() { 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 da394345..a3a3cde3 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 00000000..75f674da --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/SyncCategory.java @@ -0,0 +1,34 @@ +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 { + + @Override + protected String getId() { + return "sync"; + } + + 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(); + } +} 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 00000000..9a52e840 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/SystemCategory.java @@ -0,0 +1,40 @@ +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 00000000..3bf51519 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/TerminalCategory.java @@ -0,0 +1,81 @@ +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/TroubleshootCategory.java b/app/src/main/java/io/xpipe/app/prefs/TroubleshootCategory.java new file mode 100644 index 00000000..70fcdb35 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/TroubleshootCategory.java @@ -0,0 +1,100 @@ +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.DesktopHelper; +import io.xpipe.app.util.FileOpener; +import io.xpipe.app.util.OptionsBuilder; +import io.xpipe.app.util.TerminalLauncher; +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())); + var runScript = sc.getShellDialect().runScriptCommand(sc, script); + TerminalLauncher.openDirect("XPipe Debug", sc, runScript); + } + }); + 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/UpdateCheckComp.java b/app/src/main/java/io/xpipe/app/prefs/UpdateCheckComp.java index 1355d4d5..ac3979ca 100644 --- a/app/src/main/java/io/xpipe/app/prefs/UpdateCheckComp.java +++ b/app/src/main/java/io/xpipe/app/prefs/UpdateCheckComp.java @@ -45,12 +45,15 @@ public class UpdateCheckComp extends SimpleComp { var name = Bindings.createStringBinding( () -> { if (updateReady.getValue()) { - var prefix = XPipeDistributionType.get() == XPipeDistributionType.PORTABLE ? AppI18n.get("updateReadyPortable") : AppI18n.get("updateReady"); - var version = "Version " + XPipeDistributionType.get() - .getUpdateHandler() - .getPreparedUpdate() - .getValue() - .getVersion(); + var prefix = XPipeDistributionType.get() == XPipeDistributionType.PORTABLE + ? AppI18n.get("updateReadyPortable") + : AppI18n.get("updateReady"); + var version = "Version " + + XPipeDistributionType.get() + .getUpdateHandler() + .getPreparedUpdate() + .getValue() + .getVersion(); return prefix + " (" + version + ")"; } @@ -60,7 +63,9 @@ public class UpdateCheckComp extends SimpleComp { var description = Bindings.createStringBinding( () -> { if (updateReady.getValue()) { - return XPipeDistributionType.get() == XPipeDistributionType.PORTABLE ? AppI18n.get("updateReadyDescriptionPortable") : AppI18n.get("updateReadyDescription"); + return XPipeDistributionType.get() == XPipeDistributionType.PORTABLE + ? AppI18n.get("updateReadyDescriptionPortable") + : AppI18n.get("updateReadyDescription"); } return AppI18n.get("checkForUpdatesDescription"); 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 ec88b969..d592baf2 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,56 @@ 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/ContextualFileReference.java b/app/src/main/java/io/xpipe/app/storage/ContextualFileReference.java index fd7e5207..13b3eeb9 100644 --- a/app/src/main/java/io/xpipe/app/storage/ContextualFileReference.java +++ b/app/src/main/java/io/xpipe/app/storage/ContextualFileReference.java @@ -17,6 +17,9 @@ public class ContextualFileReference { private static String lastDataDir; + @NonNull + private final String path; + private static String getDataDir() { if (DataStorage.get() == null) { return lastDataDir != null ? lastDataDir : normalized(AppPrefs.DEFAULT_STORAGE_DIR); @@ -73,11 +76,9 @@ public class ContextualFileReference { return new ContextualFileReference(normalized(replaced)); } - @NonNull - private final String path; - public String toAbsoluteFilePath(ShellControl sc) { - return path.replaceAll("/", Matcher.quoteReplacement(sc != null ? sc.getOsType().getFileSystemSeparator() : "/")); + return path.replaceAll( + "/", Matcher.quoteReplacement(sc != null ? sc.getOsType().getFileSystemSeparator() : "/")); } public boolean isInDataDirectory() { diff --git a/app/src/main/java/io/xpipe/app/storage/DataStateProviderImpl.java b/app/src/main/java/io/xpipe/app/storage/DataStateProviderImpl.java index 1bbc79d3..9eede617 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStateProviderImpl.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStateProviderImpl.java @@ -15,8 +15,8 @@ public class DataStateProviderImpl extends DataStateProvider { return; } - var entry = DataStorage.get().getStoreEntryIfPresent(store) - .or(() -> DataStorage.get().getStoreEntryInProgressIfPresent(store)); + var entry = DataStorage.get().getStoreEntryIfPresent(store).or(() -> DataStorage.get() + .getStoreEntryInProgressIfPresent(store)); if (entry.isEmpty()) { return; } @@ -25,7 +25,6 @@ public class DataStateProviderImpl extends DataStateProvider { } @Override - @SuppressWarnings("unchecked") public T getState(DataStore store, Supplier def) { if (DataStorage.get() == null) { return def.get(); @@ -74,7 +73,7 @@ public class DataStateProviderImpl extends DataStateProvider { var r = entry.get().getStoreCache().get(key); if (r == null) { - r = def .get(); + r = def.get(); entry.get().setStoreCache(key, r); } return c.cast(r); 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 c9f458cb..cac425c5 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java @@ -1,18 +1,24 @@ 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; import io.xpipe.app.util.FixedHierarchyStore; import io.xpipe.app.util.ThreadHelper; -import io.xpipe.core.store.*; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.store.DataStoreId; +import io.xpipe.core.store.FixedChildStore; +import io.xpipe.core.store.LocalStore; import io.xpipe.core.util.UuidHelper; import javafx.util.Pair; 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; @@ -35,9 +41,6 @@ public abstract class DataStorage { private static DataStorage INSTANCE; protected final Path dir; - @Getter - protected boolean loaded; - @Getter protected final List storeCategories; @@ -46,36 +49,28 @@ public abstract class DataStorage { @Getter protected final Set storeEntriesSet; - @Getter - @Setter - protected DataStoreCategory selectedCategory; + protected final ReentrantLock busyIo = new ReentrantLock(); @Getter private final List listeners = new CopyOnWriteArrayList<>(); private final Map storeEntriesInProgress = new ConcurrentHashMap<>(); - protected final ReentrantLock busyIo = new ReentrantLock(); + @Getter + protected boolean loaded; + + @Getter + @Setter + protected DataStoreCategory selectedCategory; 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 DataStoreCategory getDefaultCategory() { - return getStoreCategoryIfPresent(DEFAULT_CATEGORY_UUID).orElseThrow(); - } - - public DataStoreCategory getAllConnectionsCategory() { - return getStoreCategoryIfPresent(ALL_CONNECTIONS_CATEGORY_UUID).orElseThrow(); - } - - public DataStoreCategory getAllScriptsCategory() { - return getStoreCategoryIfPresent(ALL_SCRIPTS_CATEGORY_UUID).orElseThrow(); - } - private static boolean shouldPersist() { if (System.getProperty(PERSIST_PROP) != null) { return Boolean.parseBoolean(System.getProperty(PERSIST_PROP)); @@ -110,17 +105,96 @@ public abstract class DataStorage { INSTANCE = null; } + public static DataStorage get() { + return INSTANCE; + } + + public abstract String getVaultKey(); + + public DataStoreCategory getDefaultConnectionsCategory() { + return getStoreCategoryIfPresent(DEFAULT_CATEGORY_UUID).orElseThrow(); + } + + public DataStoreCategory getAllConnectionsCategory() { + return getStoreCategoryIfPresent(ALL_CONNECTIONS_CATEGORY_UUID).orElseThrow(); + } + + public DataStoreCategory getAllScriptsCategory() { + return getStoreCategoryIfPresent(ALL_SCRIPTS_CATEGORY_UUID).orElseThrow(); + } + + public void forceRewrite() { + getStoreEntries().forEach(dataStoreEntry -> { + dataStoreEntry.reassignStore(); + }); + } + private synchronized void dispose() { onReset(); save(true); } - protected void onReset() {} + 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); + } - public static DataStorage get() { - return INSTANCE; + 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() {} + protected Path getStoresDir() { return dir.resolve("stores"); } @@ -146,7 +220,8 @@ public abstract class DataStorage { public void saveAsync() { // If we are already loading or saving, don't queue up another operation. // This could otherwise lead to thread starvation with virtual threads - // Technically the load and save operations also return instantly if locked, but let's not even create new threads here + // Technically the load and save operations also return instantly if locked, but let's not even create new + // threads here if (busyIo.isLocked()) { return; } @@ -159,19 +234,20 @@ public abstract class DataStorage { public abstract void save(boolean dispose); public abstract boolean supportsSharing(); - public boolean shouldShare(DataStoreCategory entry) { - if (!entry.canShare()) { + + public boolean shouldShare(DataStoreCategory category) { + if (!category.canShare()) { return false; } - DataStoreCategory c = entry; + DataStoreCategory c = category; do { if (!c.shouldShareChildren()) { return false; } } while ((c = DataStorage.get() - .getStoreCategoryIfPresent(c.getParentCategory()) - .orElse(null)) + .getStoreCategoryIfPresent(c.getParentCategory()) + .orElse(null)) != null); return true; } @@ -185,6 +261,11 @@ public abstract class DataStorage { DataStoreEntry c = entry; do { + // We can't check for sharing of invalid entries + if (!c.getValidity().isUsable()) { + return false; + } + if (c.getStore() instanceof LocalStore && entry.getProvider().isShareableFromLocalMachine()) { return true; } @@ -225,6 +306,13 @@ public abstract class DataStorage { entry.initializeEntry(); if (!sameParent) { + if (oldParent.isPresent()) { + oldParent.get().setChildrenCache(null); + } + if (newParent.isPresent()) { + newParent.get().setChildrenCache(null); + newParent.get().setExpanded(true); + } var toAdd = Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new); listeners.forEach(storageListener -> storageListener.onStoreAdd(toAdd)); } @@ -234,7 +322,9 @@ public abstract class DataStorage { } public void updateCategory(DataStoreEntry entry, DataStoreCategory newCategory) { - if (getStoreCategoryIfPresent(entry.getUuid()).map(category -> category.equals(newCategory)).orElse(false)) { + if (getStoreCategoryIfPresent(entry.getUuid()) + .map(category -> category.equals(newCategory)) + .orElse(false)) { return; } @@ -276,9 +366,11 @@ public abstract class DataStorage { return false; } - return newChildren.stream().filter(nc -> nc.getStore().getFixedId().isPresent()).noneMatch(nc -> { - return nc.getStore().getFixedId().getAsInt() == oid.getAsInt(); - }); + return newChildren.stream() + .filter(nc -> nc.getStore().getFixedId().isPresent()) + .noneMatch(nc -> { + return nc.getStore().getFixedId().getAsInt() == oid.getAsInt(); + }); }) .toList(); var toAdd = newChildren.stream() @@ -289,9 +381,16 @@ public abstract class DataStorage { return false; } - return oldChildren.stream().filter(oc -> ((FixedChildStore) oc.getStore()).getFixedId().isPresent()).noneMatch(oc -> { - return ((FixedChildStore) oc.getStore()).getFixedId().getAsInt() == nid.getAsInt(); - }); + return oldChildren.stream() + .filter(oc -> ((FixedChildStore) oc.getStore()) + .getFixedId() + .isPresent()) + .noneMatch(oc -> { + return ((FixedChildStore) oc.getStore()) + .getFixedId() + .getAsInt() + == nid.getAsInt(); + }); }) .toList(); var toUpdate = oldChildren.stream() @@ -318,13 +417,20 @@ public abstract class DataStorage { deleteWithChildren(toRemove.toArray(DataStoreEntry[]::new)); addStoreEntriesIfNotPresent(toAdd.stream().map(DataStoreEntryRef::get).toArray(DataStoreEntry[]::new)); toUpdate.forEach(pair -> { - pair.getKey().setStoreInternal(pair.getValue().getStore(), false); + // TODO do we need this, it erases any custom information? + // pair.getKey().setStoreInternal(pair.getValue().getStore(), false); // Update state by merging if (pair.getKey().getStorePersistentState() != null && pair.getValue().get().getStorePersistentState() != null) { - var mergedState = pair.getKey().getStorePersistentState().deepCopy(); - mergedState.merge(pair.getValue().get().getStorePersistentState()); - pair.getKey().setStorePersistentState(mergedState); + var classMatch = pair.getKey().getStorePersistentState().getClass() + .equals(pair.getValue().get().getStorePersistentState().getClass()); + // Children classes might not be the same, the same goes for state classes + // This can happen when there are multiple child classes and the ids got switched around + if (classMatch) { + var mergedState = pair.getKey().getStorePersistentState().deepCopy(); + mergedState.merge(pair.getValue().get().getStorePersistentState()); + pair.getKey().setStorePersistentState(mergedState); + } } }); saveAsync(); @@ -350,7 +456,7 @@ public abstract class DataStorage { .toList(); toDelete.forEach(entry -> entry.finalizeEntry()); - this.storeEntriesSet.removeAll(toDelete); + toDelete.forEach(this.storeEntriesSet::remove); this.listeners.forEach(l -> l.onStoreRemove(toDelete.toArray(DataStoreEntry[]::new))); refreshValidities(false); saveAsync(); @@ -427,7 +533,8 @@ public abstract class DataStorage { public void addStoreEntriesIfNotPresent(@NonNull DataStoreEntry... es) { for (DataStoreEntry e : es) { - if (storeEntriesSet.contains(e) || getStoreEntryIfPresent(e.getStore()).isPresent()) { + if (storeEntriesSet.contains(e) + || getStoreEntryIfPresent(e.getStore()).isPresent()) { return; } @@ -466,7 +573,11 @@ public abstract class DataStorage { return f.get(); } - var e = DataStoreEntry.createNew(UUID.randomUUID(), related != null ? related.getCategoryUuid() : selectedCategory.getUuid(), name, store); + var e = DataStoreEntry.createNew( + UUID.randomUUID(), + related != null ? related.getCategoryUuid() : selectedCategory.getUuid(), + name, + store); addStoreEntryIfNotPresent(e); return e; } @@ -504,7 +615,7 @@ public abstract class DataStorage { public boolean isRootEntry(DataStoreEntry entry) { var noParent = DataStorage.get().getDefaultDisplayParent(entry).isEmpty(); - var diffParentCategory = DataStorage.get() + boolean diffParentCategory = DataStorage.get() .getDefaultDisplayParent(entry) .map(p -> !p.getCategoryUuid().equals(entry.getCategoryUuid())) .orElse(false); @@ -628,7 +739,7 @@ public abstract class DataStorage { break; } - es.add(0, current); + es.addFirst(current); } return es; @@ -642,7 +753,7 @@ public abstract class DataStorage { } public Optional getStoreEntryIfPresent(@NonNull DataStoreId id) { - var current = getStoreEntryIfPresent(id.getNames().get(0)); + var current = getStoreEntryIfPresent(id.getNames().getFirst()); if (current.isPresent()) { for (int i = 1; i < id.getNames().size(); i++) { var children = getStoreChildren(current.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 00000000..509c18d3 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/storage/DataStorageEncryption.java @@ -0,0 +1,68 @@ +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 -> { + if (chars.length == 0) { + return JsonNodeFactory.instance.missingNode(); + } + + 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/DataStoreCategory.java b/app/src/main/java/io/xpipe/app/storage/DataStoreCategory.java index ce52d0d3..40906cf0 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStoreCategory.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStoreCategory.java @@ -32,16 +32,86 @@ public class DataStoreCategory extends StorageElement { boolean share; public DataStoreCategory( - Path directory, UUID uuid, String name, Instant lastUsed, Instant lastModified, boolean dirty, - UUID parentCategory, StoreSortMode sortMode, - boolean share - ) { + Path directory, + UUID uuid, + String name, + Instant lastUsed, + Instant lastModified, + boolean dirty, + UUID parentCategory, + StoreSortMode sortMode, + boolean share) { super(directory, uuid, name, lastUsed, lastModified, dirty); this.parentCategory = parentCategory; this.sortMode = sortMode; this.share = share; } + public static DataStoreCategory createNew(UUID parentCategory, @NonNull String name) { + return new DataStoreCategory( + null, + UUID.randomUUID(), + name, + Instant.now(), + Instant.now(), + true, + parentCategory, + StoreSortMode.ALPHABETICAL_ASC, + false); + } + + public static DataStoreCategory createNew(UUID parentCategory, @NonNull UUID uuid, @NonNull String name) { + return new DataStoreCategory( + null, + uuid, + name, + Instant.now(), + Instant.now(), + true, + parentCategory, + StoreSortMode.ALPHABETICAL_ASC, + false); + } + + public static Optional fromDirectory(Path dir) throws Exception { + ObjectMapper mapper = JacksonMapper.getDefault(); + + var entryFile = dir.resolve("category.json"); + var stateFile = dir.resolve("state.json"); + if (!Files.exists(entryFile)) { + return Optional.empty(); + } + + var stateJson = + Files.exists(stateFile) ? mapper.readTree(stateFile.toFile()) : JsonNodeFactory.instance.objectNode(); + var json = mapper.readTree(entryFile.toFile()); + + var uuid = UUID.fromString(json.required("uuid").textValue()); + var parentUuid = Optional.ofNullable(json.get("parentUuid")) + .filter(jsonNode -> !jsonNode.isNull()) + .map(jsonNode -> UUID.fromString(jsonNode.textValue())) + .orElse(null); + + var name = json.required("name").textValue(); + var sortMode = Optional.ofNullable(stateJson.get("sortMode")) + .map(JsonNode::asText) + .flatMap(string -> StoreSortMode.fromId(string)) + .orElse(StoreSortMode.ALPHABETICAL_ASC); + var share = + Optional.ofNullable(json.get("share")).map(JsonNode::asBoolean).orElse(false); + var lastUsed = Optional.ofNullable(stateJson.get("lastUsed")) + .map(jsonNode -> jsonNode.textValue()) + .map(Instant::parse) + .orElse(Instant.now()); + var lastModified = Optional.ofNullable(stateJson.get("lastModified")) + .map(jsonNode -> jsonNode.textValue()) + .map(Instant::parse) + .orElse(Instant.now()); + + return Optional.of( + new DataStoreCategory(dir, uuid, name, lastUsed, lastModified, false, parentUuid, sortMode, share)); + } + public void setSortMode(StoreSortMode sortMode) { var changed = this.sortMode != sortMode; if (changed) { @@ -66,45 +136,6 @@ public class DataStoreCategory extends StorageElement { notifyUpdate(); } - public static DataStoreCategory createNew(UUID parentCategory, @NonNull String name) { - return new DataStoreCategory(null, UUID.randomUUID(), name, Instant.now(), Instant.now(), true, parentCategory, StoreSortMode.ALPHABETICAL_ASC, - false - ); - } - - public static DataStoreCategory createNew(UUID parentCategory, @NonNull UUID uuid, @NonNull String name) { - return new DataStoreCategory(null, uuid, name, Instant.now(), Instant.now(), true, parentCategory, StoreSortMode.ALPHABETICAL_ASC, false); - } - - public static Optional fromDirectory(Path dir) throws Exception { - ObjectMapper mapper = JacksonMapper.getDefault(); - - var entryFile = dir.resolve("category.json"); - var stateFile = dir.resolve("state.json"); - if (!Files.exists(entryFile)) { - return Optional.empty(); - } - - var stateJson = Files.exists(stateFile) ? mapper.readTree(stateFile.toFile()) : JsonNodeFactory.instance.objectNode(); - var json = mapper.readTree(entryFile.toFile()); - - var uuid = UUID.fromString(json.required("uuid").textValue()); - var parentUuid = Optional.ofNullable(json.get("parentUuid")).filter(jsonNode -> !jsonNode.isNull()).map(jsonNode -> UUID.fromString(jsonNode.textValue())).orElse(null); - - var name = json.required("name").textValue(); - var sortMode = Optional.ofNullable(stateJson.get("sortMode")) - .map(JsonNode::asText) - .flatMap(string -> StoreSortMode.fromId(string)) - .orElse(StoreSortMode.ALPHABETICAL_ASC); - var share = Optional.ofNullable(json.get("share")) - .map(JsonNode::asBoolean) - .orElse(false); - var lastUsed = Optional.ofNullable(stateJson.get("lastUsed")).map(jsonNode -> jsonNode.textValue()).map(Instant::parse).orElse(Instant.now()); - var lastModified = Optional.ofNullable(stateJson.get("lastModified")).map(jsonNode -> jsonNode.textValue()).map(Instant::parse).orElse(Instant.now()); - - return Optional.of(new DataStoreCategory(dir, uuid, name, lastUsed, lastModified, false, parentUuid, sortMode, share)); - } - public boolean canShare() { if (parentCategory == null) { return false; diff --git a/app/src/main/java/io/xpipe/app/storage/DataStoreColor.java b/app/src/main/java/io/xpipe/app/storage/DataStoreColor.java index 117a4425..0f7cbb03 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStoreColor.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStoreColor.java @@ -35,7 +35,6 @@ public enum DataStoreColor { public String toHexString() { var value = terminalColor; - return "#" + (format(value.getRed()) + format(value.getGreen()) + format(value.getBlue())) - .toUpperCase(); + return "#" + (format(value.getRed()) + format(value.getGreen()) + format(value.getBlue())).toUpperCase(); } } 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 a50543cf..e2d11d94 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java @@ -25,6 +25,8 @@ import java.util.stream.Collectors; @Value public class DataStoreEntry extends StorageElement { + Map storeCache = new LinkedHashMap<>(); + @NonFinal Validity validity; @@ -53,8 +55,6 @@ public class DataStoreEntry extends StorageElement { @NonFinal DataStoreProvider provider; - Map storeCache = new LinkedHashMap<>(); - @NonFinal UUID categoryUuid; @@ -83,8 +83,8 @@ public class DataStoreEntry extends StorageElement { Validity validity, Configuration configuration, JsonNode storePersistentState, - boolean expanded, DataStoreColor color - ) { + boolean expanded, + DataStoreColor color) { super(directory, uuid, name, lastUsed, lastModified, dirty); this.categoryUuid = categoryUuid; this.store = DataStorageParser.storeFromNode(storeNode); @@ -106,8 +106,7 @@ public class DataStoreEntry extends StorageElement { String name, Instant lastUsed, Instant lastModified, - DataStore store - ) { + DataStore store) { super(directory, uuid, name, lastUsed, lastModified, false); this.categoryUuid = categoryUuid; this.store = store; @@ -120,21 +119,6 @@ public class DataStoreEntry extends StorageElement { this.storePersistentStateNode = null; } - @Override - public boolean equals(Object o) { - return o == this || (o instanceof DataStoreEntry e && e.getUuid().equals(getUuid())); - } - - @Override - public int hashCode() { - return getUuid().hashCode(); - } - - @Override - public String toString() { - return getName(); - } - public static DataStoreEntry createTempWrapper(@NonNull DataStore store) { return new DataStoreEntry( null, @@ -143,19 +127,21 @@ public class DataStoreEntry extends StorageElement { UUID.randomUUID().toString(), Instant.now(), Instant.now(), - store - ); + store); } public static DataStoreEntry createNew(@NonNull String name, @NonNull DataStore store) { - return createNew(UUID.randomUUID(), DataStorage.get().getSelectedCategory().getUuid(), name, store); + return createNew( + UUID.randomUUID(), DataStorage.get().getSelectedCategory().getUuid(), name, store); } @SneakyThrows public static DataStoreEntry createNew( @NonNull UUID uuid, @NonNull UUID categoryUuid, @NonNull String name, @NonNull DataStore store) { var node = DataStorageWriter.storeToNode(store); - var validity = DataStorageParser.storeFromNode(node) == null ? Validity.LOAD_FAILED : store.isComplete() ? Validity.COMPLETE : Validity.INCOMPLETE; + var validity = DataStorageParser.storeFromNode(node) == null + ? Validity.LOAD_FAILED + : store.isComplete() ? Validity.COMPLETE : Validity.INCOMPLETE; var entry = new DataStoreEntry( null, uuid, @@ -168,8 +154,8 @@ public class DataStoreEntry extends StorageElement { validity, Configuration.defaultConfiguration(), null, - false, null - ); + false, + null); return entry; } @@ -198,8 +184,8 @@ public class DataStoreEntry extends StorageElement { Validity.INCOMPLETE, configuration, storePersistentState, - expanded, color - ); + expanded, + color); } public static Optional fromDirectory(Path dir) throws Exception { @@ -258,12 +244,37 @@ 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(); } - return Optional.of( - createExisting(dir, uuid, categoryUuid, name, lastUsed, lastModified, storeNode, configuration, persistentState, expanded, color)); + return Optional.of(createExisting( + dir, + uuid, + categoryUuid, + name, + lastUsed, + lastModified, + storeNode, + configuration, + persistentState, + expanded, + color)); + } + + @Override + public int hashCode() { + return getUuid().hashCode(); + } + + @Override + public boolean equals(Object o) { + return o == this || (o instanceof DataStoreEntry e && e.getUuid().equals(getUuid())); + } + + @Override + public String toString() { + return getName(); } public void setInRefresh(boolean newRefresh) { @@ -284,16 +295,6 @@ public class DataStoreEntry extends StorageElement { } } - public void setStorePersistentState(DataStoreState value) { - var changed = !Objects.equals(storePersistentState, value); - this.storePersistentState = value; - this.storePersistentStateNode = JacksonMapper.getDefault().valueToTree(value); - if (changed) { - this.dirty = true; - notifyUpdate(); - } - } - @SneakyThrows @SuppressWarnings("unchecked") public T getStorePersistentState() { @@ -315,6 +316,16 @@ public class DataStoreEntry extends StorageElement { return (T) sds.getStateClass().cast(storePersistentState); } + public void setStorePersistentState(DataStoreState value) { + var changed = !Objects.equals(storePersistentState, value); + this.storePersistentState = value; + this.storePersistentStateNode = JacksonMapper.getDefault().valueToTree(value); + if (changed) { + this.dirty = true; + notifyUpdate(); + } + } + public void setConfiguration(Configuration configuration) { this.configuration = configuration; this.dirty = true; @@ -332,6 +343,35 @@ public class DataStoreEntry extends StorageElement { return new Path[] {directory.resolve("store.json"), directory.resolve("entry.json")}; } + public void writeDataToDisk() throws Exception { + if (!dirty) { + return; + } + + ObjectMapper mapper = JacksonMapper.getDefault(); + ObjectNode obj = JsonNodeFactory.instance.objectNode(); + ObjectNode stateObj = JsonNodeFactory.instance.objectNode(); + obj.put("uuid", uuid.toString()); + obj.put("name", name); + obj.put("categoryUuid", categoryUuid.toString()); + stateObj.put("lastUsed", lastUsed.toString()); + stateObj.put("lastModified", lastModified.toString()); + stateObj.set("color", mapper.valueToTree(color)); + stateObj.set("persistentState", storePersistentStateNode); + obj.set("configuration", mapper.valueToTree(configuration)); + stateObj.put("expanded", expanded); + + var entryString = mapper.writeValueAsString(obj); + var stateString = mapper.writeValueAsString(stateObj); + var storeString = mapper.writeValueAsString(DataStorageEncryption.encryptNodeIfNeeded(storeNode)); + + FileUtils.forceMkdir(directory.toFile()); + Files.writeString(directory.resolve("state.json"), stateString); + Files.writeString(directory.resolve("entry.json"), entryString); + Files.writeString(directory.resolve("store.json"), storeString); + dirty = false; + } + public void setExpanded(boolean expanded) { var changed = expanded != this.expanded; this.expanded = expanded; @@ -384,6 +424,11 @@ public class DataStoreEntry extends StorageElement { dirty = true; } + public void reassignStore() { + this.storeNode = DataStorageWriter.storeToNode(store); + dirty = true; + } + public void validate() { try { validateOrThrow(); @@ -399,7 +444,9 @@ public class DataStoreEntry extends StorageElement { if (store instanceof ValidatableStore l) { l.validate(); } else if (store instanceof FixedHierarchyStore h) { - childrenCache = h.listChildren(this).stream().map(DataStoreEntryRef::get).collect(Collectors.toSet()); + childrenCache = h.listChildren(this).stream() + .map(DataStoreEntryRef::get) + .collect(Collectors.toSet()); } } finally { setInRefresh(false); @@ -493,43 +540,6 @@ public class DataStoreEntry extends StorageElement { return getStore() != null; } - public void writeDataToDisk() throws Exception { - if (!dirty) { - return; - } - - ObjectMapper mapper = JacksonMapper.getDefault(); - ObjectNode obj = JsonNodeFactory.instance.objectNode(); - ObjectNode stateObj = JsonNodeFactory.instance.objectNode(); - obj.put("uuid", uuid.toString()); - obj.put("name", name); - obj.put("categoryUuid", categoryUuid.toString()); - stateObj.put("lastUsed", lastUsed.toString()); - stateObj.put("lastModified", lastModified.toString()); - stateObj.set("color", mapper.valueToTree(color)); - stateObj.set("persistentState", storePersistentStateNode); - obj.set("configuration", mapper.valueToTree(configuration)); - stateObj.put("expanded", expanded); - - var entryString = mapper.writeValueAsString(obj); - var stateString = mapper.writeValueAsString(stateObj); - var storeString = mapper.writeValueAsString(storeNode); - - FileUtils.forceMkdir(directory.toFile()); - Files.writeString(directory.resolve("state.json"), stateString); - Files.writeString(directory.resolve("entry.json"), entryString); - Files.writeString(directory.resolve("store.json"), storeString); - 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 00000000..6efbfd90 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/storage/DataStoreSecret.java @@ -0,0 +1,70 @@ +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 { + + InPlaceSecretValue internalSecret; + String usedPasswordLockCrypt; + + @Setter + @NonFinal + TreeNode originalNode; + + 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 int hashCode() { + return Arrays.hashCode(getSecret()); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DataStoreSecret that)) { + return false; + } + return Arrays.equals(getSecret(), that.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/GitStorageHandler.java b/app/src/main/java/io/xpipe/app/storage/GitStorageHandler.java index 37a994c3..3b02096e 100644 --- a/app/src/main/java/io/xpipe/app/storage/GitStorageHandler.java +++ b/app/src/main/java/io/xpipe/app/storage/GitStorageHandler.java @@ -7,16 +7,14 @@ import java.nio.file.Path; public interface GitStorageHandler { static GitStorageHandler getInstance() { - return (GitStorageHandler) ProcessControlProvider.get().createStorageHandler(); + return (GitStorageHandler) ProcessControlProvider.get().getGitStorageHandler(); } - void onReset(); - boolean supportsShare(); - void init(Path dir); + void init(); - void beforeStorageLoad(); + void setupRepositoryAndPull(); void afterStorageLoad(); 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 99dc448e..51b3be27 100644 --- a/app/src/main/java/io/xpipe/app/storage/ImpersistentStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/ImpersistentStorage.java @@ -1,24 +1,63 @@ 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 - public boolean supportsSharing() { - return false; - } - - @Override - public void save(boolean dispose) { + public synchronized void save(boolean dispose) { var storesDir = getStoresDir(); TrackEvent.info("Storage persistence is disabled. Deleting storage contents ..."); @@ -31,4 +70,8 @@ public class ImpersistentStorage extends DataStorage { } } + @Override + public boolean supportsSharing() { + return false; + } } 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 4602647c..315ecbd5 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.core.process.OsType; 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; @@ -21,91 +22,22 @@ import java.util.stream.Stream; public class StandardStorage extends DataStorage { private final List directoriesToKeep = new ArrayList<>(); + @Getter private final GitStorageHandler gitStorageHandler; + private String vaultKey; + @Getter private boolean disposed; StandardStorage() { this.gitStorageHandler = GitStorageHandler.getInstance(); - this.gitStorageHandler.init(dir); } @Override - protected void onReset() { - gitStorageHandler.onReset(); - } - - private void deleteLeftovers() { - var storesDir = getStoresDir(); - var categoriesDir = getCategoriesDir(); - - // Delete leftover directories in entries dir - try (var s = Files.list(storesDir)) { - s.forEach(file -> { - if (directoriesToKeep.contains(file)) { - return; - } - - var name = file.getFileName().toString(); - try { - UUID uuid; - try { - uuid = UUID.fromString(name); - } catch (Exception ex) { - FileUtils.forceDelete(file.toFile()); - return; - } - - var entry = getStoreEntryIfPresent(uuid); - if (entry.isEmpty()) { - TrackEvent.withTrace("storage", "Deleting leftover store directory") - .tag("uuid", uuid) - .handle(); - FileUtils.forceDelete(file.toFile()); - gitStorageHandler.handleDeletion(file,uuid.toString()); - } - } catch (Exception ex) { - ErrorEvent.fromThrowable(ex).omitted(true).build().handle(); - } - }); - } catch (Exception ex) { - ErrorEvent.fromThrowable(ex).terminal(true).build().handle(); - } - - // Delete leftover directories in categories dir - try (var s = Files.list(categoriesDir)) { - s.forEach(file -> { - if (directoriesToKeep.contains(file)) { - return; - } - - var name = file.getFileName().toString(); - try { - UUID uuid; - try { - uuid = UUID.fromString(name); - } catch (Exception ex) { - FileUtils.forceDelete(file.toFile()); - return; - } - - var entry = getStoreCategoryIfPresent(uuid); - if (entry.isEmpty()) { - TrackEvent.withTrace("storage", "Deleting leftover category directory") - .tag("uuid", uuid) - .handle(); - FileUtils.forceDelete(file.toFile()); - gitStorageHandler.handleDeletion(file,uuid.toString()); - } - } catch (Exception ex) { - ErrorEvent.fromThrowable(ex).omitted(true).build().handle(); - } - }); - } catch (Exception ex) { - ErrorEvent.fromThrowable(ex).terminal(true).build().handle(); - } + public String getVaultKey() { + return vaultKey; } public void load() { @@ -113,12 +45,27 @@ public class StandardStorage extends DataStorage { return; } - this.gitStorageHandler.beforeStorageLoad(); + try { + FileUtils.forceMkdir(dir.toFile()); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).terminal(true).build().handle(); + } + + try { + initSystemInfo(); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).build().handle(); + } + + try { + initVaultKey(); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).terminal(true).build().handle(); + } var storesDir = getStoresDir(); var categoriesDir = getCategoriesDir(); var dataDir = getDataDir(); - try { FileUtils.forceMkdir(storesDir.toFile()); FileUtils.forceMkdir(categoriesDir.toFile()); @@ -142,9 +89,9 @@ public class StandardStorage extends DataStorage { c.ifPresent(storeCategories::add); } catch (IOException ex) { // IO exceptions are not expected - exception.set(new IOException("Unable to load data from " + path.toString() + ". Is it corrupted?", ex)); + exception.set(new IOException("Unable to load data from " + path + ". Is it corrupted?", ex)); directoriesToKeep.add(path); - } catch (Exception ex) { + } catch (Exception ex) { // Data corruption and schema changes are expected ErrorEvent.fromThrowable(ex).expected().omit().build().handle(); } @@ -156,56 +103,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 { @@ -228,9 +128,9 @@ public class StandardStorage extends DataStorage { storeEntries.put(entry.get(), entry.get()); } catch (IOException ex) { // IO exceptions are not expected - exception.set(new IOException("Unable to load data from " + path.toString() + ". Is it corrupted?", ex)); + exception.set(new IOException("Unable to load data from " + path + ". Is it corrupted?", ex)); directoriesToKeep.add(path); - } catch (Exception ex) { + } catch (Exception ex) { // Data corruption and schema changes are expected // We only keep invalid entries in developer mode as there's no point in keeping them in @@ -245,13 +145,13 @@ public class StandardStorage extends DataStorage { // Show one exception if (exception.get() != null) { - ErrorEvent.fromThrowable(exception.get()).handle(); + ErrorEvent.fromThrowable(exception.get()).expected().handle(); } storeEntriesSet.forEach(dataStoreCategory -> { if (dataStoreCategory.getCategoryUuid() == null || getStoreCategoryIfPresent(dataStoreCategory.getCategoryUuid()) - .isEmpty()) { + .isEmpty()) { dataStoreCategory.setCategoryUuid(DEFAULT_CATEGORY_UUID); } }); @@ -260,21 +160,36 @@ 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).expected().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 -> { @@ -287,11 +202,14 @@ public class StandardStorage extends DataStorage { // Save to apply changes if (!hasFixedLocal) { - storeEntriesSet.removeIf(dataStoreEntry -> !dataStoreEntry.getUuid().equals(LOCAL_ID) && dataStoreEntry.getStore() instanceof LocalStore); - storeEntriesSet.stream().filter(entry -> entry.getValidity() != DataStoreEntry.Validity.LOAD_FAILED).forEach(entry -> { - entry.dirty = true; - entry.setStoreNode(DataStorageWriter.storeToNode(entry.getStore())); - }); + storeEntriesSet.removeIf(dataStoreEntry -> + !dataStoreEntry.getUuid().equals(LOCAL_ID) && dataStoreEntry.getStore() instanceof LocalStore); + storeEntriesSet.stream() + .filter(entry -> entry.getValidity() != DataStoreEntry.Validity.LOAD_FAILED) + .forEach(entry -> { + entry.dirty = true; + entry.setStoreNode(DataStorageWriter.storeToNode(entry.getStore())); + }); save(false); } @@ -336,7 +254,7 @@ public class StandardStorage extends DataStorage { } catch (IOException ex) { // IO exceptions are not expected exception.set(ex); - } catch (Exception ex) { + } catch (Exception ex) { // Data corruption and schema changes are expected ErrorEvent.fromThrowable(ex).expected().omit().build().handle(); } @@ -359,7 +277,7 @@ public class StandardStorage extends DataStorage { // Show one exception if (exception.get() != null) { - ErrorEvent.fromThrowable(exception.get()).handle(); + ErrorEvent.fromThrowable(exception.get()).expected().handle(); } deleteLeftovers(); @@ -374,4 +292,108 @@ public class StandardStorage extends DataStorage { public boolean supportsSharing() { return gitStorageHandler.supportsShare(); } + + private void deleteLeftovers() { + var storesDir = getStoresDir(); + var categoriesDir = getCategoriesDir(); + + // Delete leftover directories in entries dir + try (var s = Files.list(storesDir)) { + s.forEach(file -> { + if (directoriesToKeep.contains(file)) { + return; + } + + var name = file.getFileName().toString(); + try { + UUID uuid; + try { + uuid = UUID.fromString(name); + } catch (Exception ex) { + FileUtils.forceDelete(file.toFile()); + return; + } + + var entry = getStoreEntryIfPresent(uuid); + if (entry.isEmpty()) { + TrackEvent.withTrace("Deleting leftover store directory") + .tag("uuid", uuid) + .handle(); + FileUtils.forceDelete(file.toFile()); + gitStorageHandler.handleDeletion(file, uuid.toString()); + } + } catch (Exception ex) { + ErrorEvent.fromThrowable(ex).omitted(true).build().handle(); + } + }); + } catch (Exception ex) { + ErrorEvent.fromThrowable(ex).terminal(true).build().handle(); + } + + // Delete leftover directories in categories dir + try (var s = Files.list(categoriesDir)) { + s.forEach(file -> { + if (directoriesToKeep.contains(file)) { + return; + } + + var name = file.getFileName().toString(); + try { + UUID uuid; + try { + uuid = UUID.fromString(name); + } catch (Exception ex) { + FileUtils.forceDelete(file.toFile()); + return; + } + + var entry = getStoreCategoryIfPresent(uuid); + if (entry.isEmpty()) { + TrackEvent.withTrace("Deleting leftover category directory") + .tag("uuid", uuid) + .handle(); + FileUtils.forceDelete(file.toFile()); + gitStorageHandler.handleDeletion(file, uuid.toString()); + } + } catch (Exception ex) { + ErrorEvent.fromThrowable(ex).omitted(true).build().handle(); + } + }); + } catch (Exception ex) { + ErrorEvent.fromThrowable(ex).terminal(true).build().handle(); + } + } + + 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 read = Files.readString(file); + if (!OsType.getLocal().getName().equals(read)) { + ErrorEvent.fromMessage( + "This vault was originally created on a different system running " + read + + ". 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(); + var s = OsType.getLocal().getName(); + Files.writeString(file, s); + } + } else { + Files.createDirectories(dir); + var s = OsType.getLocal().getName(); + Files.writeString(file, s); + } + } } diff --git a/app/src/main/java/io/xpipe/app/storage/StorageElement.java b/app/src/main/java/io/xpipe/app/storage/StorageElement.java index 42a08004..8582a39b 100644 --- a/app/src/main/java/io/xpipe/app/storage/StorageElement.java +++ b/app/src/main/java/io/xpipe/app/storage/StorageElement.java @@ -17,9 +17,12 @@ public abstract class StorageElement { @Getter protected final UUID uuid; + protected final List listeners = new ArrayList<>(); + @Getter protected boolean dirty; + @Getter protected Path directory; @@ -28,6 +31,7 @@ public abstract class StorageElement { @Getter protected Instant lastUsed; + @Getter protected Instant lastModified; 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 dd639c97..537b8de5 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); } @@ -28,7 +37,8 @@ public class StorageJacksonModule extends SimpleModule { public static class LocalFileReferenceSerializer extends JsonSerializer { @Override - public void serialize(ContextualFileReference value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + public void serialize(ContextualFileReference value, JsonGenerator jgen, SerializerProvider provider) + throws IOException { jgen.writeString(value.serialize()); } } @@ -41,11 +51,65 @@ 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 { @Override - public void serialize(DataStoreEntryRef value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + public void serialize(DataStoreEntryRef value, JsonGenerator jgen, SerializerProvider provider) + throws IOException { if (value == null) { jgen.writeNull(); return; @@ -73,7 +137,10 @@ public class StorageJacksonModule extends SimpleModule { } var id = UUID.fromString(text); - var e = DataStorage.get().getStoreEntryIfPresent(id).filter(dataStoreEntry -> dataStoreEntry.getValidity() != DataStoreEntry.Validity.LOAD_FAILED).orElse(null); + var e = DataStorage.get() + .getStoreEntryIfPresent(id) + .filter(dataStoreEntry -> dataStoreEntry.getValidity() != DataStoreEntry.Validity.LOAD_FAILED) + .orElse(null); if (e == null) { return null; } diff --git a/app/src/main/java/io/xpipe/app/storage/StorageListener.java b/app/src/main/java/io/xpipe/app/storage/StorageListener.java index f01825cb..730a2688 100644 --- a/app/src/main/java/io/xpipe/app/storage/StorageListener.java +++ b/app/src/main/java/io/xpipe/app/storage/StorageListener.java @@ -6,7 +6,6 @@ public interface StorageListener { void onStoreRemove(DataStoreEntry... entry); - void onCategoryAdd(DataStoreCategory category); void onCategoryRemove(DataStoreCategory category); 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 b949ea05..42ca5cb4 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,25 @@ 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 d0f0e0a1..52227d7d 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 afbe9102..f04b6ac4 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 57058cdd..9bfb3dbe 100644 --- a/app/src/main/java/io/xpipe/app/update/AppDownloads.java +++ b/app/src/main/java/io/xpipe/app/update/AppDownloads.java @@ -4,6 +4,7 @@ import io.xpipe.app.core.AppProperties; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.util.HttpHelper; +import io.xpipe.core.util.JacksonMapper; import org.apache.commons.io.FileUtils; import org.kohsuke.github.GHRelease; import org.kohsuke.github.GHRepository; @@ -58,7 +59,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)) @@ -78,6 +79,20 @@ public class AppDownloads { return Optional.empty(); } + try { + var url = URI.create("https://api.xpipe.io/changelog?from=" + + AppProperties.get().getVersion() + "&to=" + version + "&stage=" + + AppProperties.get().isStaging()) + .toURL(); + var bytes = HttpHelper.executeGet(url, aFloat -> {}); + var string = new String(bytes, StandardCharsets.UTF_8); + var json = JacksonMapper.getDefault().readTree(string); + var changelog = json.required("changelog").asText(); + return Optional.of(changelog); + } catch (Throwable t) { + ErrorEvent.fromThrowable(t).omit().handle(); + } + try { var asset = release.get().listAssets().toList().stream() .filter(ghAsset -> ghAsset.getName().equals("changelog.md")) @@ -96,37 +111,26 @@ public class AppDownloads { } } - public static String getLatestVersion() throws IOException { - return getLatestSuitableRelease() - .map(ghRelease -> ghRelease.getTagName()) - .orElse("?"); - } - - public static Optional getLatestIncludingPreRelease() throws IOException { + public static Optional getTopReleaseIncludingPreRelease() throws IOException { var repo = getRepository(); return Optional.ofNullable(repo.listReleases().iterator().next()); } - public static Optional getLatestRelease() throws IOException { + public static Optional getMarkedLatestRelease() throws IOException { var repo = getRepository(); return Optional.ofNullable(repo.getLatestRelease()); } public static Optional getLatestSuitableRelease() throws IOException { - var preIncluding = getLatestIncludingPreRelease(); - + var preIncluding = getTopReleaseIncludingPreRelease(); // If we are currently running a prerelease, always return this as the suitable release! - if (preIncluding.isPresent() && preIncluding.get().isPrerelease() + if (preIncluding.isPresent() + && preIncluding.get().isPrerelease() && AppProperties.get().getVersion().equals(preIncluding.get().getTagName())) { return preIncluding; } - // If this release is not a prerelease, just return it to prevent querying another release - if (preIncluding.isPresent() && !preIncluding.get().isPrerelease()) { - return preIncluding; - } - - return getLatestRelease(); + return getMarkedLatestRelease(); } public static Optional getRelease(String version, boolean omitErrors) { 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 b4c157e7..a845035b 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,23 @@ 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.app.util.TerminalLauncher; import io.xpipe.core.process.OsType; -import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellDialects; 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 +40,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 +50,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) { @@ -115,40 +62,11 @@ public class AppInstaller { @JsonTypeName("msi") public static final class Msi extends InstallerAssetType { - @Override - public String getExtension() { - 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( @@ -156,84 +74,54 @@ public class AppInstaller { String.format( """ cd /D "%%HOMEDRIVE%%%%HOMEPATH%%" - start "" /wait msiexec /i "%s" /l* "%s" /qb + start "" /wait msiexec /i "%s" /lv "%s" /qr start "" "%s" """, file, logFile, exec)); - shellProcessControl.executeSimpleCommand("start \"\" /min \"" + script + "\""); + shellProcessControl.executeSimpleCommand("start \"XPipe Updater\" /min cmd /c \"" + script + "\""); + } + + @Override + public String getExtension() { + return ".msi"; } } @JsonTypeName("debian") public static final class Debian extends InstallerAssetType { - @Override - public String getExtension() { - 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); + } + + @Override + public String getExtension() { + return ".deb"; } } @JsonTypeName("rpm") public static final class Rpm extends InstallerAssetType { - @Override - public String getExtension() { - 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,39 +130,27 @@ 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); + } + + @Override + public String getExtension() { + return ".rpm"; } } @JsonTypeName("pkg") public static final class Pkg extends InstallerAssetType { - @Override - public String getExtension() { - 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 +158,19 @@ 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); + } + + @Override + public String getExtension() { + return ".pkg"; } } } diff --git a/app/src/main/java/io/xpipe/app/update/ChocoUpdater.java b/app/src/main/java/io/xpipe/app/update/ChocoUpdater.java index 83c43b45..bf5ab465 100644 --- a/app/src/main/java/io/xpipe/app/update/ChocoUpdater.java +++ b/app/src/main/java/io/xpipe/app/update/ChocoUpdater.java @@ -49,7 +49,6 @@ public class ChocoUpdater extends UpdateHandler { null, null, Instant.now(), - null, isUpdate); lastUpdateCheckResult.setValue(rel); return lastUpdateCheckResult.getValue(); 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 4f92d9b7..b11eeb28 100644 --- a/app/src/main/java/io/xpipe/app/update/GitHubUpdater.java +++ b/app/src/main/java/io/xpipe/app/update/GitHubUpdater.java @@ -35,14 +35,13 @@ public class GitHubUpdater extends UpdateHandler { XPipeDistributionType.get().getId(), lastUpdateCheckResult.getValue().getVersion(), lastUpdateCheckResult.getValue().getReleaseUrl(), - lastUpdateCheckResult.getValue().getReleaseDate(), downloadFile.get(), changelog, lastUpdateCheckResult.getValue().getAssetType()); preparedUpdate.setValue(rel); } - public void executeUpdateAndCloseImpl() throws Exception { + public void executeUpdateOnCloseImpl() throws Exception { var downloadFile = preparedUpdate.getValue().getFile(); if (!Files.exists(downloadFile)) { return; @@ -79,7 +78,6 @@ public class GitHubUpdater extends UpdateHandler { ghAsset.get().getBrowserDownloadUrl(), assetType, Instant.now(), - rel.get().getCreatedAt() != null ? rel.get().getCreatedAt().toInstant() : null, isUpdate)); return lastUpdateCheckResult.getValue(); } diff --git a/app/src/main/java/io/xpipe/app/update/HomebrewUpdater.java b/app/src/main/java/io/xpipe/app/update/HomebrewUpdater.java index ce9a01fd..8e091d57 100644 --- a/app/src/main/java/io/xpipe/app/update/HomebrewUpdater.java +++ b/app/src/main/java/io/xpipe/app/update/HomebrewUpdater.java @@ -44,7 +44,6 @@ public class HomebrewUpdater extends GitHubUpdater { lastUpdateCheckResult.getValue().getVersion(), lastUpdateCheckResult.getValue().getReleaseUrl(), null, - null, changelog, lastUpdateCheckResult.getValue().getAssetType()); preparedUpdate.setValue(rel); 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 0b11472b..ea243435 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(); } @@ -50,7 +50,6 @@ public class PortableUpdater extends UpdateHandler { null, null, Instant.now(), - rel.get().getCreatedAt() != null ? rel.get().getCreatedAt().toInstant() : null, isUpdate)); return lastUpdateCheckResult.getValue(); } diff --git a/app/src/main/java/io/xpipe/app/update/UpdateAvailableAlert.java b/app/src/main/java/io/xpipe/app/update/UpdateAvailableAlert.java index f1383b05..dcc83c3e 100644 --- a/app/src/main/java/io/xpipe/app/update/UpdateAvailableAlert.java +++ b/app/src/main/java/io/xpipe/app/update/UpdateAvailableAlert.java @@ -10,10 +10,6 @@ import javafx.scene.control.ButtonType; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.time.format.FormatStyle; - public class UpdateAvailableAlert { public static void showIfNeeded() { @@ -26,15 +22,8 @@ public class UpdateAvailableAlert { var update = AppWindowHelper.showBlockingAlert(alert -> { alert.setTitle(AppI18n.get("updateReadyAlertTitle")); alert.setAlertType(Alert.AlertType.NONE); - - var date = u.getReleaseDate() != null ? - DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) - .format(u.getReleaseDate().atZone(ZoneId.systemDefault())) : "Latest"; - var markdown = new MarkdownComp(u.getBody() != null ? u.getBody() : "", s -> { - var header = " 

" + AppI18n.get("whatsNew", u.getVersion(), date) + "

"; - return header + s; - }) - .createRegion(); + var markdown = + new MarkdownComp(u.getBody() != null ? u.getBody() : "", s -> " " + s).createRegion(); alert.getButtonTypes().clear(); var updaterContent = uh.createInterface(); if (updaterContent != null) { 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 5af6461e..f33fcbf3 100644 --- a/app/src/main/java/io/xpipe/app/update/UpdateChangelogAlert.java +++ b/app/src/main/java/io/xpipe/app/update/UpdateChangelogAlert.java @@ -4,15 +4,12 @@ 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; import javafx.stage.Modality; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.time.format.FormatStyle; - public class UpdateChangelogAlert { private static boolean shown = false; @@ -20,7 +17,15 @@ 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; } @@ -39,17 +44,11 @@ public class UpdateChangelogAlert { alert.setAlertType(Alert.AlertType.NONE); alert.initModality(Modality.NONE); - var date = update.getReleaseDate() != null ? - DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) - .format(update.getReleaseDate().atZone(ZoneId.systemDefault())) : "Latest"; - var markdown = new MarkdownComp(update.getRawDescription(), s -> { - var header = " 

" + AppI18n.get("whatsNew", update.getName(), date) + "

"; - return header + s; - }) - .createRegion(); + var markdown = new MarkdownComp(update.getRawDescription(), s -> " " + s).createRegion(); alert.getDialogPane().setContent(markdown); alert.getButtonTypes().add(new ButtonType(AppI18n.get("gotIt"), ButtonBar.ButtonData.OK_DONE)); - }, r -> r.filter(b -> b.getButtonData().isDefaultButton()).ifPresent(t -> {})); + }, + r -> r.filter(b -> b.getButtonData().isDefaultButton()).ifPresent(t -> {})); } } 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 09fac1f7..94b0257e 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) { @@ -184,7 +184,6 @@ public abstract class UpdateHandler { XPipeDistributionType.get().getId(), lastUpdateCheckResult.getValue().getVersion(), lastUpdateCheckResult.getValue().getReleaseUrl(), - lastUpdateCheckResult.getValue().getReleaseDate(), null, changelog, lastUpdateCheckResult.getValue().getAssetType()); @@ -212,7 +211,8 @@ public abstract class UpdateHandler { return; } - if (available != null && !available.getVersion().equals(preparedUpdate.getValue().getVersion())) { + if (available != null + && !available.getVersion().equals(preparedUpdate.getValue().getVersion())) { preparedUpdate.setValue(null); return; } @@ -220,21 +220,25 @@ 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()); + preparedUpdate.getValue().getVersion()); 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(100); + } catch (Throwable ex) { + ex.printStackTrace(); } }); } - public void executeUpdateAndCloseImpl() throws Exception { + public void executeUpdateOnCloseImpl() throws Exception { throw new UnsupportedOperationException(); } @@ -257,7 +261,6 @@ public abstract class UpdateHandler { String name; String rawDescription; String newVersion; - Instant releaseDate; } @Value @@ -272,7 +275,6 @@ public abstract class UpdateHandler { String downloadUrl; AppInstaller.InstallerAssetType assetType; Instant checkTime; - Instant releaseDate; boolean isUpdate; } @@ -284,7 +286,6 @@ public abstract class UpdateHandler { String sourceDist; String version; String releaseUrl; - Instant releaseDate; Path file; String body; AppInstaller.InstallerAssetType assetType; diff --git a/app/src/main/java/io/xpipe/app/update/XPipeDistributionType.java b/app/src/main/java/io/xpipe/app/update/XPipeDistributionType.java index 2d081a2b..1b33b857 100644 --- a/app/src/main/java/io/xpipe/app/update/XPipeDistributionType.java +++ b/app/src/main/java/io/xpipe/app/update/XPipeDistributionType.java @@ -24,6 +24,15 @@ public enum XPipeDistributionType { private static XPipeDistributionType type; + @Getter + private final String id; + + @Getter + private final boolean supportsUrls; + + private final Supplier updateHandlerSupplier; + private UpdateHandler updateHandler; + XPipeDistributionType(String id, boolean supportsUrls, Supplier updateHandlerSupplier) { this.id = id; this.supportsUrls = supportsUrls; @@ -62,7 +71,9 @@ public enum XPipeDistributionType { type = det; AppCache.update("dist", type.getId()); - TrackEvent.withInfo("Determined distribution type").tag("type",type.getId()).handle(); + TrackEvent.withInfo("Determined distribution type") + .tag("type", type.getId()) + .handle(); } public static XPipeDistributionType get() { @@ -84,7 +95,8 @@ public enum XPipeDistributionType { } try (var sc = LocalShell.getShell()) { - // In theory, we can also add && !AppProperties.get().isStaging() here, but we want to replicate the production behavior + // In theory, we can also add && !AppProperties.get().isStaging() here, but we want to replicate the + // production behavior if (OsType.getLocal().equals(OsType.WINDOWS)) { try (var chocoOut = sc.command("choco search --local-only -r xpipe").start()) { @@ -101,7 +113,8 @@ public enum XPipeDistributionType { } } - // In theory, we can also add && !AppProperties.get().isStaging() here, but we want to replicate the production behavior + // In theory, we can also add && !AppProperties.get().isStaging() here, but we want to replicate the + // production behavior if (OsType.getLocal().equals(OsType.MACOS)) { try (var brewOut = sc.command("brew list --casks --versions").start()) { var out = brewOut.readStdoutDiscardErr(); @@ -124,14 +137,6 @@ public enum XPipeDistributionType { return XPipeDistributionType.NATIVE_INSTALLATION; } - @Getter - private final String id; - @Getter - private final boolean supportsUrls; - - private UpdateHandler updateHandler; - private final Supplier updateHandlerSupplier; - public UpdateHandler getUpdateHandler() { if (updateHandler == null) { updateHandler = updateHandlerSupplier.get(); 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 b0aa71de..0c42c420 100644 --- a/app/src/main/java/io/xpipe/app/update/XPipeInstanceHelper.java +++ b/app/src/main/java/io/xpipe/app/update/XPipeInstanceHelper.java @@ -8,7 +8,10 @@ import io.xpipe.core.store.ShellStore; import java.io.IOException; import java.nio.file.Files; -import java.util.*; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; import java.util.stream.Collectors; public class XPipeInstanceHelper { @@ -35,7 +38,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 cbd3b0a0..f787d43f 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()) { @@ -70,8 +70,8 @@ public class ApplicationHelper { ShellControl processControl, String executable, String displayName, DataStoreEntry connection) throws Exception { if (!isInPath(processControl, executable)) { - throw ErrorEvent.unreportable(new IOException(displayName + " executable " + executable + " not found in PATH" - + (connection != null ? " on system " + connection.getName() : ""))); + throw ErrorEvent.unreportable(new IOException(displayName + " executable " + executable + + " not found in PATH" + (connection != null ? " on system " + connection.getName() : ""))); } } @@ -79,7 +79,7 @@ public class ApplicationHelper { throws Exception { if (!supplier.get()) { throw ErrorEvent.unreportable(new IOException(displayName + " is not supported" - + (connection != null ? " on system " + connection.getName() : ""))); + + (connection != null ? " on system " + connection.getName() : ""))); } } } 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 619e62c3..ce6b3ca6 100644 --- a/app/src/main/java/io/xpipe/app/util/AskpassAlert.java +++ b/app/src/main/java/io/xpipe/app/util/AskpassAlert.java @@ -3,65 +3,69 @@ 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.animation.AnimationTimer; 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, InPlaceSecretValue secretValue) { + var prop = new SimpleObjectProperty<>(secretValue); var r = AppWindowHelper.showBlockingAlert(alert -> { alert.setTitle(AppI18n.get("askpassAlertTitle")); alert.setHeaderText(prompt); alert.setAlertType(Alert.AlertType.CONFIRMATION); - var text = new SecretFieldComp(prop).createRegion(); + var text = new SecretFieldComp(prop).createStructure().get(); alert.getDialogPane().setContent(new StackPane(text)); + var stage = (Stage) alert.getDialogPane().getScene().getWindow(); + stage.setAlwaysOnTop(true); + + var anim = new AnimationTimer() { + + private long lastRun = 0; + + @Override + public void handle(long now) { + if (lastRun == 0) { + lastRun = now; + return; + } + + long elapsed = (now - lastRun) / 1_000_000; + if (elapsed < 1000) { + return; + } + + stage.requestFocus(); + lastRun = now; + } + }; alert.setOnShown(event -> { + stage.requestFocus(); + anim.start(); // Wait 1 pulse before focus so that the scene can be assigned to text - Platform.runLater(text::requestFocus); + Platform.runLater(() -> { + text.requestFocus(); + text.end(); + }); event.consume(); }); + alert.setOnHiding(event -> { + anim.stop(); + }); }) - .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/BooleanScope.java b/app/src/main/java/io/xpipe/app/util/BooleanScope.java index 02ee2361..6e4c7191 100644 --- a/app/src/main/java/io/xpipe/app/util/BooleanScope.java +++ b/app/src/main/java/io/xpipe/app/util/BooleanScope.java @@ -6,6 +6,15 @@ import javafx.beans.property.BooleanProperty; public class BooleanScope implements AutoCloseable { + private final BooleanProperty prop; + private boolean invert; + private boolean forcePlatform; + private boolean wait; + + public BooleanScope(BooleanProperty prop) { + this.prop = prop; + } + public static void execute(BooleanProperty prop, FailableRunnable r) throws E { try (var ignored = new BooleanScope(prop).start()) { r.run(); @@ -18,11 +27,6 @@ public class BooleanScope implements AutoCloseable { } } - private final BooleanProperty prop; - private boolean invert; - private boolean forcePlatform; - private boolean wait; - public BooleanScope exclusive() { this.wait = true; return this; @@ -38,10 +42,6 @@ public class BooleanScope implements AutoCloseable { return this; } - public BooleanScope(BooleanProperty prop) { - this.prop = prop; - } - public BooleanScope start() { if (wait) { while (!invert == prop.get()) { diff --git a/app/src/main/java/io/xpipe/app/util/DataStoreCategoryChoiceComp.java b/app/src/main/java/io/xpipe/app/util/DataStoreCategoryChoiceComp.java index 82fd90cb..f274250d 100644 --- a/app/src/main/java/io/xpipe/app/util/DataStoreCategoryChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/util/DataStoreCategoryChoiceComp.java @@ -19,7 +19,8 @@ public class DataStoreCategoryChoiceComp extends SimpleComp { private final Property external; private final Property value; - public DataStoreCategoryChoiceComp(StoreCategoryWrapper root, Property external, Property value) { + public DataStoreCategoryChoiceComp( + StoreCategoryWrapper root, Property external, Property value) { this.root = root; this.external = external; this.value = value; @@ -53,7 +54,6 @@ public class DataStoreCategoryChoiceComp extends SimpleComp { return box; } - @EqualsAndHashCode(callSuper = true) @Value private static class Cell extends ListCell { diff --git a/app/src/main/java/io/xpipe/app/util/DataStoreFormatter.java b/app/src/main/java/io/xpipe/app/util/DataStoreFormatter.java index 2ae94801..7166319f 100644 --- a/app/src/main/java/io/xpipe/app/util/DataStoreFormatter.java +++ b/app/src/main/java/io/xpipe/app/util/DataStoreFormatter.java @@ -15,12 +15,16 @@ public class DataStoreFormatter { public static ObservableValue shellInformation(StoreEntryWrapper w) { return BindingsHelper.map(w.getPersistentState(), o -> { - if (o instanceof ShellStoreState shellStoreState) { - if (!shellStoreState.isInitialized()) { + if (o instanceof ShellStoreState s) { + if (!s.isInitialized()) { return null; } - return shellStoreState.isRunning() ? shellStoreState.getOsName() : "Connection failed"; + if (s.getShellDialect() != null && !s.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) { + return s.getOsName() != null ? s.getOsName() : s.getShellDialect().getDisplayName(); + } + + return s.isRunning() ? s.getOsName() : "Connection failed"; } return "?"; @@ -32,8 +36,7 @@ public class DataStoreFormatter { return null; } - return name.substring(0, 1).toUpperCase() - + name.substring(1).toLowerCase(); + return name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase(); } public static String formatSubHost(IntFunction func, DataStore at, int length) { @@ -61,9 +64,8 @@ public class DataStoreFormatter { } public static String formatViaProxy(IntFunction func, DataStoreEntry at, int length) { - var atString = at.getStore() instanceof ShellStore shellStore && !ShellStore.isLocal(shellStore) - ? at.getName() - : null; + var atString = + at.getStore() instanceof ShellStore shellStore && !ShellStore.isLocal(shellStore) ? at.getName() : null; if (atString == null) { return func.apply(length); } @@ -85,7 +87,7 @@ public class DataStoreFormatter { return "?"; } - return cut(input.getName(), length); + return cut(input.getName(), length); } public static String split(String left, String separator, String right, int length) { diff --git a/app/src/main/java/io/xpipe/app/util/DesktopHelper.java b/app/src/main/java/io/xpipe/app/util/DesktopHelper.java index 638cc212..5beb63df 100644 --- a/app/src/main/java/io/xpipe/app/util/DesktopHelper.java +++ b/app/src/main/java/io/xpipe/app/util/DesktopHelper.java @@ -11,7 +11,8 @@ public class DesktopHelper { public static Path getDesktopDirectory() throws Exception { if (OsType.getLocal() == OsType.WINDOWS) { - return Path.of(LocalShell.getLocalPowershell().executeSimpleStringCommand("[Environment]::GetFolderPath([Environment+SpecialFolder]::Desktop)")); + return Path.of(LocalShell.getLocalPowershell() + .executeSimpleStringCommand("[Environment]::GetFolderPath([Environment+SpecialFolder]::Desktop)")); } else if (OsType.getLocal() == OsType.LINUX) { try (var cmd = LocalShell.getShell().command("xdg-user-dir DESKTOP").start()) { var read = cmd.readStdoutDiscardErr(); diff --git a/app/src/main/java/io/xpipe/app/util/DialogHelper.java b/app/src/main/java/io/xpipe/app/util/DialogHelper.java index 8acbe27b..9ca512c1 100644 --- a/app/src/main/java/io/xpipe/app/util/DialogHelper.java +++ b/app/src/main/java/io/xpipe/app/util/DialogHelper.java @@ -5,11 +5,7 @@ import io.xpipe.core.charsetter.NewLine; import io.xpipe.core.charsetter.StreamCharset; import io.xpipe.core.dialog.Dialog; import io.xpipe.core.dialog.QueryConverter; -import io.xpipe.core.store.LocalStore; -import io.xpipe.core.store.DataFlow; -import io.xpipe.core.store.DataStore; -import io.xpipe.core.store.FileSystem; -import io.xpipe.core.store.ShellStore; +import io.xpipe.core.store.*; import io.xpipe.core.util.SecretValue; import lombok.Value; diff --git a/app/src/main/java/io/xpipe/app/util/DiscreteProgressScope.java b/app/src/main/java/io/xpipe/app/util/DiscreteProgressScope.java index 17ea4961..09c0e9f0 100644 --- a/app/src/main/java/io/xpipe/app/util/DiscreteProgressScope.java +++ b/app/src/main/java/io/xpipe/app/util/DiscreteProgressScope.java @@ -4,8 +4,8 @@ import javafx.beans.property.DoubleProperty; public class DiscreteProgressScope extends ProgressScope { - private int counter = 0; private final int steps; + private int counter = 0; public DiscreteProgressScope(DoubleProperty prop, int steps) { super(prop); 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 54edabd2..ec17b119 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,31 +15,24 @@ 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 { - private static final Path TEMP = - FileUtils.getTempDirectory().toPath().resolve("xpipe").resolve("bridge"); + private static final Path TEMP = ShellTemp.getLocalTempDataDirectory("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 +55,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 +116,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 +152,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,19 +162,31 @@ 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; } - Path file = TEMP.resolve(UUID.randomUUID().toString().substring(0, 6)).resolve(getFileSystemCompatibleName(keyName)); + Path file = TEMP.resolve(UUID.randomUUID().toString().substring(0, 6)) + .resolve(getFileSystemCompatibleName(keyName)); try { FileUtils.forceMkdirParent(file.toFile()); try (var out = Files.newOutputStream(file); @@ -190,9 +198,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 +219,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 ea53ecb1..54eec2a4 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,37 +58,60 @@ 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) + .description("Unable to launch editor " + editor.toTranslatedString().getValue() + + ".\nMaybe try to use a different editor in the settings.") .expected() .handle(); } } - 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 -> { + throw new UnsupportedOperationException(); + } + 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/HttpHelper.java b/app/src/main/java/io/xpipe/app/util/HttpHelper.java index c0172d75..1a8fd537 100644 --- a/app/src/main/java/io/xpipe/app/util/HttpHelper.java +++ b/app/src/main/java/io/xpipe/app/util/HttpHelper.java @@ -3,23 +3,12 @@ package io.xpipe.app.util; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; -import java.net.URI; import java.net.URL; import java.nio.ByteBuffer; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.function.Consumer; public class HttpHelper { - public static Path downloadFile(String urlS) throws Exception { - var url = URI.create(urlS).toURL(); - var bytes = HttpHelper.executeGet(url, aFloat -> {}); - var downloadFile = Files.createTempFile(null, null); - Files.write(downloadFile, bytes); - return downloadFile; - } - public static byte[] executeGet(URL targetURL, Consumer progress) throws Exception { HttpURLConnection connection = null; @@ -36,7 +25,12 @@ public class HttpHelper { } InputStream is = connection.getInputStream(); - int size = Integer.parseInt(connection.getHeaderField("Content-Length")); + var lengthField = connection.getHeaderField("Content-Length"); + if (lengthField == null) { + return is.readAllBytes(); + } + + int size = Integer.parseInt(lengthField); byte[] line; int bytes = 0; 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 02c92756..2ed387ac 100644 --- a/app/src/main/java/io/xpipe/app/util/Hyperlinks.java +++ b/app/src/main/java/io/xpipe/app/util/Hyperlinks.java @@ -4,16 +4,17 @@ 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"; + public static final String SLACK = + "https://join.slack.com/t/XPipe/shared_invite/zt-1awjq0t5j-5i4UjNJfNe1VN4b_auu6Cg"; - static final String[] browsers = {"xdg-open", "google-chrome", "firefox", "opera", "konqueror", "mozilla", "gnome-open", "open"}; + static final String[] browsers = { + "xdg-open", "google-chrome", "firefox", "opera", "konqueror", "mozilla", "gnome-open", "open" + }; @SuppressWarnings("deprecation") public static void open(String uri) { 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 00000000..03aac129 --- /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/JfxHelper.java b/app/src/main/java/io/xpipe/app/util/JfxHelper.java index ca9654df..292b77db 100644 --- a/app/src/main/java/io/xpipe/app/util/JfxHelper.java +++ b/app/src/main/java/io/xpipe/app/util/JfxHelper.java @@ -65,7 +65,7 @@ public class JfxHelper { } var size = 40; - var graphic = PrettyImageHelper.ofFixedSquare(image, (int) size).createRegion(); + var graphic = PrettyImageHelper.ofFixedSquare(image, size).createRegion(); var hbox = new HBox(graphic, text); hbox.setAlignment(Pos.CENTER_LEFT); 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 c89c62ad..8fc8844a 100644 --- a/app/src/main/java/io/xpipe/app/util/LicenseProvider.java +++ b/app/src/main/java/io/xpipe/app/util/LicenseProvider.java @@ -15,26 +15,6 @@ public abstract class LicenseProvider { return INSTANCE; } - public static class Loader implements ModuleLayerLoader { - - @Override - public void init(ModuleLayer layer) { - INSTANCE = ServiceLoader.load(layer, LicenseProvider.class).stream() - .map(ServiceLoader.Provider::get) - .findFirst().orElseThrow(() -> ExtensionException.corrupt("Missing license provider.")); - } - - @Override - public boolean requiresFullDaemon() { - return true; - } - - @Override - public boolean prioritizeLoading() { - return true; - } - } - public abstract boolean hasLicense(); public abstract LicensedFeature getFeature(String id); @@ -48,4 +28,25 @@ public abstract class LicenseProvider { public abstract Comp overviewPage(); public abstract boolean hasPaidLicense(); + + public static class Loader implements ModuleLayerLoader { + + @Override + public void init(ModuleLayer layer) { + INSTANCE = ServiceLoader.load(layer, LicenseProvider.class).stream() + .map(ServiceLoader.Provider::get) + .findFirst() + .orElseThrow(() -> ExtensionException.corrupt("Missing license provider")); + } + + @Override + public boolean requiresFullDaemon() { + return true; + } + + @Override + public boolean prioritizeLoading() { + return true; + } + } } diff --git a/app/src/main/java/io/xpipe/app/util/LicenseRequiredException.java b/app/src/main/java/io/xpipe/app/util/LicenseRequiredException.java index abb157e6..8eb2eda0 100644 --- a/app/src/main/java/io/xpipe/app/util/LicenseRequiredException.java +++ b/app/src/main/java/io/xpipe/app/util/LicenseRequiredException.java @@ -8,11 +8,11 @@ public class LicenseRequiredException extends RuntimeException { private final LicensedFeature feature; public LicenseRequiredException(LicensedFeature feature) { - super(feature.getDisplayName() + (feature.isPlural() ? " are" : " is") + " only supported with a professional license"); + super(feature.getDisplayName() + (feature.isPlural() ? " are" : " is") + + " only supported with a professional license"); this.feature = feature; } - public LicenseRequiredException(String featureName, boolean plural, LicensedFeature feature) { super(featureName + (plural ? " are" : " is") + " only supported with a professional license"); this.feature = feature; diff --git a/app/src/main/java/io/xpipe/app/util/LicensedFeature.java b/app/src/main/java/io/xpipe/app/util/LicensedFeature.java index 96685eba..768e503f 100644 --- a/app/src/main/java/io/xpipe/app/util/LicensedFeature.java +++ b/app/src/main/java/io/xpipe/app/util/LicensedFeature.java @@ -1,7 +1,21 @@ package io.xpipe.app.util; +import java.util.Optional; + public interface LicensedFeature { + default Optional getDescriptionSuffix() { + if (isSupported()) { + return Optional.empty(); + } + + if (isPreviewSupported()) { + return Optional.of("Preview"); + } + + return Optional.of("Pro"); + } + String getId(); String getDisplayName(); @@ -12,5 +26,9 @@ public interface LicensedFeature { boolean isPreviewSupported(); - public void throwIfUnsupported() throws LicenseRequiredException; + default void throwIfUnsupported() throws LicenseRequiredException { + if (!isSupported()) { + throw new LicenseRequiredException(this); + } + } } diff --git a/app/src/main/java/io/xpipe/app/util/LocalShell.java b/app/src/main/java/io/xpipe/app/util/LocalShell.java index 72bef2bd..480f3644 100644 --- a/app/src/main/java/io/xpipe/app/util/LocalShell.java +++ b/app/src/main/java/io/xpipe/app/util/LocalShell.java @@ -9,6 +9,7 @@ public class LocalShell { @Getter private static ShellControlCache localCache; + private static ShellControl local; private static ShellControl localPowershell; @@ -19,7 +20,8 @@ public class LocalShell { public static ShellControl getLocalPowershell() throws Exception { if (localPowershell == null) { - localPowershell = ProcessControlProvider.get().createLocalProcessControl(true) + localPowershell = ProcessControlProvider.get() + .createLocalProcessControl(true) .subShell(ShellDialects.POWERSHELL) .start(); } 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 b216c3b6..3a632435 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/MacOsPermissions.java b/app/src/main/java/io/xpipe/app/util/MacOsPermissions.java index 12c2a665..c7ccb09f 100644 --- a/app/src/main/java/io/xpipe/app/util/MacOsPermissions.java +++ b/app/src/main/java/io/xpipe/app/util/MacOsPermissions.java @@ -56,7 +56,8 @@ public class MacOsPermissions { a.getButtonTypes().clear(); a.getButtonTypes().add(ButtonType.CANCEL); alert.set(a); - }, buttonType -> { + }, + buttonType -> { alert.get().close(); state.set(false); }); diff --git a/app/src/main/java/io/xpipe/app/util/MarkdownBuilder.java b/app/src/main/java/io/xpipe/app/util/MarkdownBuilder.java index 474314c0..dcf9f9cd 100644 --- a/app/src/main/java/io/xpipe/app/util/MarkdownBuilder.java +++ b/app/src/main/java/io/xpipe/app/util/MarkdownBuilder.java @@ -6,14 +6,14 @@ import net.steppschuh.markdowngenerator.text.code.CodeBlock; public class MarkdownBuilder { - public static MarkdownBuilder of() { - return new MarkdownBuilder(); - } - private final StringBuilder builder = new StringBuilder(); private MarkdownBuilder() {} + public static MarkdownBuilder of() { + return new MarkdownBuilder(); + } + public MarkdownBuilder add(String t) { builder.append(t); return this; diff --git a/app/src/main/java/io/xpipe/app/util/NamedCharacter.java b/app/src/main/java/io/xpipe/app/util/NamedCharacter.java index c58a8b91..bfd2a28a 100644 --- a/app/src/main/java/io/xpipe/app/util/NamedCharacter.java +++ b/app/src/main/java/io/xpipe/app/util/NamedCharacter.java @@ -51,7 +51,7 @@ public class NamedCharacter { .findFirst() .orElse(null); if (byChar != null) { - return byChar.getNames().get(0); + return byChar.getNames().getFirst(); } return value.toString(); diff --git a/app/src/main/java/io/xpipe/app/util/ObservableDataStore.java b/app/src/main/java/io/xpipe/app/util/ObservableDataStore.java index 358fc826..3c96a1a9 100644 --- a/app/src/main/java/io/xpipe/app/util/ObservableDataStore.java +++ b/app/src/main/java/io/xpipe/app/util/ObservableDataStore.java @@ -33,14 +33,14 @@ public interface ObservableDataStore extends DataStore, InternalCacheDataStore { } } - default void setObserverState(boolean state) { - setCache("observerState", state); - } - default boolean getObserverState() { return getCache("observerState", Boolean.class, false); } + default void setObserverState(boolean state) { + setCache("observerState", state); + } + private void refresh() { var entry = DataStorage.get().getStoreEntry(this); DataStorage.get().refreshChildren(entry); 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 0a0a5a59..0b7f50c0 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 { @@ -33,10 +37,6 @@ public class OptionsBuilder { private Comp lastCompHeadReference; private ObservableValue lastNameReference; - public Validator buildEffectiveValidator() { - return new ChainedValidator(allValidators); - } - public OptionsBuilder() { this.ownValidator = new SimpleValidator(); this.allValidators.add(ownValidator); @@ -47,14 +47,21 @@ public class OptionsBuilder { this.allValidators.add(ownValidator); } + public Validator buildEffectiveValidator() { + return new ChainedValidator(allValidators); + } + public OptionsBuilder choice(IntegerProperty selectedIndex, Map options) { var list = options.entrySet().stream() .map(e -> new ChoicePaneComp.Entry( - AppI18n.observable(e.getKey()), e.getValue() != null ? e.getValue().buildComp() : Comp.empty())) + AppI18n.observable(e.getKey()), + e.getValue() != null ? e.getValue().buildComp() : Comp.empty())) .toList(); - var validatorList = - options.values().stream().map(builder -> builder != null ? builder.buildEffectiveValidator() : new SimpleValidator()).toList(); - var selected = new SimpleObjectProperty<>(selectedIndex.getValue() != -1 ? list.get(selectedIndex.getValue()) : null); + var validatorList = options.values().stream() + .map(builder -> builder != null ? builder.buildEffectiveValidator() : new SimpleValidator()) + .toList(); + var selected = + new SimpleObjectProperty<>(selectedIndex.getValue() != -1 ? list.get(selectedIndex.getValue()) : null); selected.addListener((observable, oldValue, newValue) -> { selectedIndex.setValue(newValue != null ? list.indexOf(newValue) : null); }); @@ -93,6 +100,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); } @@ -115,7 +126,6 @@ public class OptionsBuilder { return this; } - public OptionsBuilder addTitle(String titleKey) { finishCurrent(); entries.add(new OptionsComp.Entry( @@ -134,7 +144,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 +154,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; @@ -151,26 +171,31 @@ public class OptionsBuilder { public OptionsBuilder nonNull() { var e = lastNameReference; - var p = props.get(props.size() - 1); - return decorate(Validator.nonNull(ownValidator, e, p)); + var p = props.getLast(); + 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)); + var p = props.getLast(); + 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)); + var p = props.getLast(); + 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)); + var p = props.getLast(); + return check(Validator.nonNull(v, e, p)); } private void pushComp(Comp comp) { @@ -194,6 +219,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 +239,28 @@ 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 +317,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 00000000..1365dfe5 --- /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; + } + + protected SecretKey getAESKey() throws InvalidKeySpecException { + var chars = AppPrefs.get().getLockPassword().getValue() != null + ? AppPrefs.get().getLockPassword().getValue().getSecret() + : new char[0]; + return getSecretKey(chars); + } + + @Override + public InPlaceSecretValue inPlace() { + return new InPlaceSecretValue(getSecret()); + } + + @Override + public String toString() { + return ""; + } +} 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 c03b8734..beea88cb 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; @@ -56,7 +58,6 @@ public enum PlatformState { return current == RUNNING; } - private static Optional initPlatform() { if (current == EXITED) { return Optional.of(new IllegalStateException("Platform has already exited")); @@ -80,6 +81,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/ProgressScope.java b/app/src/main/java/io/xpipe/app/util/ProgressScope.java index b17690a2..1a7ad18b 100644 --- a/app/src/main/java/io/xpipe/app/util/ProgressScope.java +++ b/app/src/main/java/io/xpipe/app/util/ProgressScope.java @@ -11,6 +11,10 @@ public class ProgressScope implements AutoCloseable { private BooleanProperty active = new SimpleBooleanProperty(); private boolean forcePlatform; + public ProgressScope(DoubleProperty prop) { + this.prop = prop; + } + public ProgressScope withActive(BooleanProperty active) { this.active = active; return this; @@ -21,10 +25,6 @@ public class ProgressScope implements AutoCloseable { return this; } - public ProgressScope(DoubleProperty prop) { - this.prop = prop; - } - public ProgressScope start() { if (forcePlatform) { PlatformThread.runLaterIfNeeded(() -> { 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 0d12f406..976a90c1 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; @@ -30,44 +23,12 @@ 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)); - } - + public Optional checkCompatibility(ShellControl s) { 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; - } + public boolean setup(ShellControl s) { + 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 daeaa919..802f9664 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; } @@ -67,105 +66,143 @@ public class ScanAlert { } private static void show( - DataStoreEntry initialStore, Function> applicable - ) { - var entry = new SimpleObjectProperty>(); - var selected = new SimpleListProperty(FXCollections.observableArrayList()); + DataStoreEntry initialStore, Function> applicable) { + DialogComp.showWindow( + "scanAlertTitle", + stage -> new Dialog(stage, initialStore != null ? initialStore.ref() : null, applicable)); + } - var loading = new SimpleBooleanProperty(); - Platform.runLater(() -> { - var stage = AppWindowHelper.sideWindow(AppI18n.get("scanAlertTitle"), window -> { - return new MultiStepComp() { + private static class Dialog extends DialogComp { - private final StackPane stackPane = new StackPane(); + 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(); - { - stackPane.getStyleClass().add("scan-list"); + private Dialog( + Stage window, + DataStoreEntryRef entry, + Function> applicable) { + this.window = window; + this.initialStore = entry; + this.entry = new SimpleObjectProperty<>(entry); + this.applicable = applicable; + } + + @Override + protected ObservableValue busy() { + return busy; + } + + @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 + public Comp content() { + StackPane stackPane = new StackPane(); + stackPane.getStyleClass().add("scan-list"); - entry.addListener((observable, oldValue, newValue) -> { - selected.clear(); - stackPane.getChildren().clear(); + 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)); - if (newValue == null) { - return; - } + SimpleChangeListener.apply(entry, newValue -> { + selected.clear(); + stackPane.getChildren().clear(); - ThreadHelper.runAsync(() -> { - BooleanScope.execute(loading, () -> { - var a = applicable.apply(entry.get().get()); + if (newValue == null) { + return; + } - Platform.runLater(() -> { - if (a == null) { - window.close(); - return; - } + ThreadHelper.runAsync(() -> { + BooleanScope.execute(busy, () -> { + var a = applicable.apply(entry.get().get()); - 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); - }); - }); - }); - }); - - 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()); + Function nameFunc = (ScanProvider.ScanOperation s) -> { + var n = AppI18n.get(s.getNameKey()); + if (s.getLicensedFeatureId() == null) { + return n; } - }); + + var suffix = LicenseProvider.get().getFeature(s.getLicensedFeatureId()); + return n + suffix.getDescriptionSuffix().map(d -> " (" + d + ")").orElse(""); + }; + var r = new ListSelectorComp<>( + a, + nameFunc, + selected, + scanOperation -> scanOperation.isDisabled(), + a.size() > 3) + .createRegion(); + stackPane.getChildren().add(r); }); - } - }; - }, false, loading); - stage.show(); - }); + }); + }); + }); + + return b; + } } } 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 4403c0a7..1c51c1c6 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,13 @@ 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 = ""; @@ -53,7 +56,14 @@ public class ScriptHelper { } if (config.getDisplayName() != null) { - content += nl + t.changeTitleCommand(config.getDisplayName()) + nl; + 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; @@ -76,7 +86,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 +98,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 +107,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,13 +119,38 @@ 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) - throws Exception { + public static String createTerminalPreparedAskpassScript( + SecretValue pass, ShellControl parent, boolean forceExecutable) throws Exception { + return createTerminalPreparedAskpassScript(pass != null ? List.of(pass) : List.of(), parent, forceExecutable); + } + + public static String createTerminalPreparedAskpassScript( + List pass, ShellControl parent, boolean forceExecutable) throws Exception { var scriptType = parent.getShellDialect(); // Fix for powershell as there are permission issues when executing a powershell askpass script @@ -123,33 +158,35 @@ public class ScriptHelper { scriptType = parent.getOsType().equals(OsType.WINDOWS) ? ShellDialects.CMD : ShellDialects.SH; } - return createAskpassScript(pass, parent, scriptType, errorMessage); + return createTerminalPreparedAskpassScript(pass, parent, scriptType); } - private static String createAskpassScript(List pass, ShellControl parent, ShellDialect type, String errorMessage) - throws Exception { + private static String createTerminalPreparedAskpassScript( + List pass, ShellControl parent, ShellDialect type) 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( + .getAskpass() + .prepareFixedContent( sub, file, pass.stream() .map(secretValue -> secretValue.getSecretValue()) - .toList(), errorMessage); + .toList()); return createExecScript(sub.getShellDialect(), sub, file, content); } } else { var content = parent.getShellDialect() - .prepareAskpassContent( + .getAskpass() + .prepareFixedContent( parent, file, pass.stream() .map(secretValue -> secretValue.getSecretValue()) - .toList(), errorMessage); + .toList()); return createExecScript(parent.getShellDialect(), parent, file, content); } } 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 a5999bf6..01896ed5 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,51 @@ package io.xpipe.app.util; +import io.xpipe.app.issue.TrackEvent; +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 expectElevationPrompt( + UUID request, UUID secretId, CountDown countDown, boolean askIfNeeded) { + var p = new SecretQueryProgress( + request, + secretId, + List.of(askIfNeeded ? SecretQuery.elevation(secretId) : 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 +56,44 @@ 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, UUID secretId, int sub) { + if (!strategy.expectsQuery()) { + return null; + } + + var uuid = UUID.randomUUID(); + var p = expectAskpass(uuid, secretId, List.of(strategy.query()), SecretQuery.prompt(false), CountDown.of()); + p.advance(sub); + var r = p.process(prompt); + completeRequest(uuid); + return r; } - public static SecretValue retrieve(SecretRetrievalStrategy strategy, String prompt, Object key, int sub) throws Exception { - var ref = new SecretReference(key, sub); - if (strategy == null) { - return null; - } - if (strategy.shouldCache() && passwords.containsKey(ref)) { - return passwords.get(ref); + public static void completeRequest(UUID request) { + if (progress.removeIf( + secretQueryProgress -> secretQueryProgress.getRequestId().equals(request))) { + TrackEvent.withTrace("Completed secret request") + .tag("uuid", request) + .handle(); } - - var pass = strategy.retrieve(prompt, ref.getSecretId(), ref.getSubId()); - if (pass == null) { - return null; - } - - if (strategy.shouldCache()) { - passwords.put(ref, pass); - } - return pass; } 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 00000000..3d30dfe3 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/SecretQuery.java @@ -0,0 +1,81 @@ +package io.xpipe.app.util; + +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.core.util.SecretReference; + +import java.util.Optional; +import java.util.UUID; + +public interface SecretQuery { + + static SecretQuery elevation(UUID secretId) { + return new SecretQuery() { + + @Override + public Optional retrieveCache(String prompt, SecretReference reference) { + var found = SecretQuery.super.retrieveCache(prompt, reference); + if (found.isEmpty()) { + return Optional.empty(); + } + + var ask = AppPrefs.get().alwaysConfirmElevation().getValue(); + if (!ask) { + return found; + } + + var inPlace = found.get().getSecret().inPlace(); + var r = AskpassAlert.queryRaw(prompt, inPlace); + return r.isCancelled() ? Optional.of(r) : found; + } + + @Override + public SecretQueryResult query(String prompt) { + return AskpassAlert.queryRaw(prompt, null); + } + + @Override + public boolean cache() { + return true; + } + + @Override + public boolean retryOnFail() { + return true; + } + }; + } + + static SecretQuery prompt(boolean cache) { + return new SecretQuery() { + @Override + public SecretQueryResult query(String prompt) { + return AskpassAlert.queryRaw(prompt, null); + } + + @Override + public boolean cache() { + return cache; + } + + @Override + public boolean retryOnFail() { + return true; + } + }; + } + + default Optional retrieveCache(String prompt, SecretReference reference) { + var r = SecretManager.get(reference); + return r.map(secretValue -> new SecretQueryResult(secretValue, false)); + } + + SecretQueryResult query(String prompt); + + boolean cache(); + + boolean retryOnFail(); + + default boolean respectDontCacheSetting() { + return true; + } +} 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 00000000..28041e3f --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/SecretQueryProgress.java @@ -0,0 +1,122 @@ +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 lombok.NonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@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( + @NonNull UUID requestId, + @NonNull UUID storeId, + @NonNull List suppliers, + @NonNull SecretQuery fallback, + @NonNull CountDown countDown) { + this.requestId = requestId; + this.storeId = storeId; + this.suppliers = new ArrayList<>(suppliers); + this.fallback = fallback; + this.countDown = countDown; + this.seenPrompts = new ArrayList<>(); + } + + public void advance(int count) { + for (int i = 0; i < count; i++) { + seenPrompts.add(null); + suppliers.add(SecretQuery.prompt(false)); + } + } + + 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 = shouldCache(sup, prompt); + 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) { + countDown.pause(); + var cached = sup.retrieveCache(prompt, ref); + countDown.resume(); + if (cached.isPresent()) { + if (cached.get().isCancelled()) { + requestCancelled = true; + return null; + } + + return cached.get().getSecret(); + } + } + + 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(); + } + + private boolean shouldCache(SecretQuery query, String prompt) { + var shouldCache = query.cache() + && SecretManager.shouldCacheForPrompt(prompt) + && (!query.respectDontCacheSetting() || !AppPrefs.get().dontCachePasswords().get()); + return shouldCache; + } +} 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 00000000..a617dcb9 --- /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 646f3635..c7c61263 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(); - - boolean shouldCache(); + default boolean expectsQuery() { + return true; + } @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 expectsQuery() { 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, null); + } - @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,34 @@ 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 +148,29 @@ 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 44d0a33b..96a659bf 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,34 @@ 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); } @@ -32,9 +42,11 @@ public class SecretRetrievalStrategyHelper { var keyProperty = new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getKey() : null); var content = new HorizontalComp(List.of( - new TextFieldComp(keyProperty).apply(struc -> struc.get().setPromptText("Password key")).hgrow(), + 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))) @@ -96,7 +108,8 @@ public class SecretRetrievalStrategyHelper { .bindChoice( () -> { return switch (selected.get() - offset) { - case 0 -> new SimpleObjectProperty<>(allowNone ? new SecretRetrievalStrategy.None() : null); + case 0 -> new SimpleObjectProperty<>( + allowNone ? new SecretRetrievalStrategy.None() : null); case 1 -> inPlace; case 2 -> passwordManager; case 3 -> customCommand; diff --git a/app/src/main/java/io/xpipe/app/util/ShellTemp.java b/app/src/main/java/io/xpipe/app/util/ShellTemp.java new file mode 100644 index 00000000..b6a0d52c --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/ShellTemp.java @@ -0,0 +1,73 @@ +package io.xpipe.app.util; + +import io.xpipe.core.process.OsType; +import io.xpipe.core.process.ShellControl; +import io.xpipe.core.store.FileNames; +import org.apache.commons.io.FileUtils; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.stream.Stream; + +public class ShellTemp { + + public static Path getLocalTempDataDirectory(String sub) { + var temp = FileUtils.getTempDirectory().toPath().resolve("xpipe"); + if (OsType.getLocal().equals(OsType.LINUX)) { + var user = System.getenv("USER"); + temp = temp.resolve(user != null ? user : "user"); + } + return temp.resolve(sub); + } + + public static String getUserSpecificTempDataDirectory(ShellControl proc, String sub) { + if (OsType.getLocal().equals(OsType.LINUX) || OsType.getLocal().equals(OsType.MACOS)) { + var user = System.getenv("USER"); + return FileNames.join("/tmp", "xpipe", user, sub); + } + var temp = proc.getSystemTemporaryDirectory(); + return FileNames.join(temp, "xpipe", sub); + } + + public static void checkTempDirectory(ShellControl proc) throws Exception { + var d = proc.getShellDialect(); + + var systemTemp = proc.getOsType().getTempDirectory(proc); + if (!d.directoryExists(proc, systemTemp).executeAndCheck() || !checkDirectoryPermissions(proc, systemTemp)) { + throw new IOException("No permissions to access %s".formatted(systemTemp)); + } + + var home = proc.getOsType().getHomeDirectory(proc); + if (!d.directoryExists(proc, home).executeAndCheck() || !checkDirectoryPermissions(proc, home)) { + throw new IOException("No permissions to access %s".formatted(home)); + } + + // Always delete legacy directory and do not care whether it partially fails + // This system xpipe temp directory might contain other files on the local machine, so only clear the exec + d.deleteFileOrDirectory(proc, FileNames.join(systemTemp, "xpipe", "exec")).executeAndCheck(); + d.deleteFileOrDirectory(proc, FileNames.join(home, ".xpipe", "temp")).executeAndCheck(); + d.deleteFileOrDirectory(proc, FileNames.join(home, ".xpipe", "system_id")).executeAndCheck(); + } + + 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 String getSubDirectory(ShellControl proc, String... sub) throws Exception { + var base = proc.getSystemTemporaryDirectory(); + 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/app/src/main/java/io/xpipe/app/util/SimpleValidator.java b/app/src/main/java/io/xpipe/app/util/SimpleValidator.java index d3e308c6..cbbc9c68 100644 --- a/app/src/main/java/io/xpipe/app/util/SimpleValidator.java +++ b/app/src/main/java/io/xpipe/app/util/SimpleValidator.java @@ -98,19 +98,6 @@ public class SimpleValidator implements Validator { return !containsErrors(); } - private void refreshProperties() { - ValidationResult nextResult = new ValidationResult(); - for (Check check : checks.keySet()) { - nextResult.addAll(check.getValidationResult().getMessages()); - } - validationResultProperty.set(nextResult); - boolean hasErrors = false; - for (ValidationMessage msg : nextResult.getMessages()) { - hasErrors = hasErrors || msg.getSeverity() == Severity.ERROR; - } - containsErrorsProperty.set(hasErrors); - } - /** * Create a string property that depends on the validation result. * Each error message will be displayed on a separate line prefixed with a bullet. @@ -134,4 +121,17 @@ public class SimpleValidator implements Validator { }, validationResultProperty); } + + private void refreshProperties() { + ValidationResult nextResult = new ValidationResult(); + for (Check check : checks.keySet()) { + nextResult.addAll(check.getValidationResult().getMessages()); + } + validationResultProperty.set(nextResult); + boolean hasErrors = false; + for (ValidationMessage msg : nextResult.getMessages()) { + hasErrors = hasErrors || msg.getSeverity() == Severity.ERROR; + } + containsErrorsProperty.set(hasErrors); + } } diff --git a/app/src/main/java/io/xpipe/app/util/TerminalLauncher.java b/app/src/main/java/io/xpipe/app/util/TerminalLauncher.java new file mode 100644 index 00000000..6550853e --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/TerminalLauncher.java @@ -0,0 +1,65 @@ +package io.xpipe.app.util; + +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.issue.ErrorEvent; +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.ShellControl; +import io.xpipe.core.process.TerminalInitScriptConfig; + +import java.io.IOException; +import java.util.UUID; + +public class TerminalLauncher { + + public static void openDirect(String title, ShellControl shellControl, String command) throws Exception { + var type = AppPrefs.get().terminalType().getValue(); + if (type == null) { + throw ErrorEvent.unreportable(new IllegalStateException(AppI18n.get("noTerminalSet"))); + } + var script = ScriptHelper.createLocalExecScript(command); + var config = new ExternalTerminalType.LaunchConfiguration( + null, title, title, script, shellControl.getShellDialect()); + type.launch(config); + } + + public static void open(String title, ProcessControl cc) throws Exception { + open(null, title, null, cc); + } + + 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"))); + } + + var color = entry != null ? DataStorage.get().getRootForEntry(entry).getColor() : null; + var prefix = entry != null && color != null && type.supportsColoredTitle() ? color.getEmoji() + " " : ""; + var cleanTitle = (title != null ? title : entry != null ? entry.getName() : "?"); + var adjustedTitle = prefix + cleanTitle; + 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) { + ErrorEvent.unreportable(new IOException( + "Unable to launch terminal " + type.toTranslatedString().getValue() + ": " + ex.getMessage() + + ".\nMaybe try to use a different terminal in the settings.", + 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 00000000..aa0d56e3 --- /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); + var t = failure.getThrowable(); + throw new ServerException(t); + } + + 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 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/ThreadHelper.java b/app/src/main/java/io/xpipe/app/util/ThreadHelper.java index 98945d63..ee4664b3 100644 --- a/app/src/main/java/io/xpipe/app/util/ThreadHelper.java +++ b/app/src/main/java/io/xpipe/app/util/ThreadHelper.java @@ -24,7 +24,9 @@ public class ThreadHelper { } public static Thread unstarted(Runnable r) { - return AppProperties.get().isUseVirtualThreads() ? Thread.ofVirtual().unstarted(wrap(r)) : Thread.ofPlatform().unstarted(wrap(r)); + return AppProperties.get().isUseVirtualThreads() + ? Thread.ofVirtual().unstarted(wrap(r)) + : Thread.ofPlatform().unstarted(wrap(r)); } public static Thread runAsync(Runnable r) { 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 c3ff046e..a24f81b0 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 4f16a352..5557a527 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,23 @@ package io.xpipe.app.util; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.core.AppStyle; +import io.xpipe.app.core.AppTheme; import io.xpipe.app.core.AppWindowHelper; 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,31 +27,45 @@ 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)); if (canceled.get()) { - ErrorEvent.fromMessage("Unlock cancelled").expected().term().omit().handle(); + ErrorEvent.fromMessage("Unlock cancelled") + .expected() + .term() + .omit() + .handle(); return; } diff --git a/app/src/main/java/io/xpipe/app/util/UserConfig.java b/app/src/main/java/io/xpipe/app/util/UserConfig.java index ce8a5d77..a81691ad 100644 --- a/app/src/main/java/io/xpipe/app/util/UserConfig.java +++ b/app/src/main/java/io/xpipe/app/util/UserConfig.java @@ -1,4 +1,3 @@ package io.xpipe.app.util; -public class UserConfig { -} +public class UserConfig {} 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 93a20955..acea15b2 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 { + 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")); + } + }); + } + + 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) { @@ -88,8 +107,6 @@ public interface Validator { /** * Create a string property that depends on the validation result. * Each error message will be displayed on a separate line prefixed with a bullet. - * - * @return */ StringBinding createStringBinding(); @@ -98,7 +115,6 @@ public interface Validator { * * @param prefix The string to prefix each validation message with * @param separator The string to separate consecutive validation messages with - * @return */ StringBinding createStringBinding(String prefix, String separator); } 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 00000000..2fe8c632 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/VaultKeySecretValue.java @@ -0,0 +1,43 @@ +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; + } + + protected SecretKey getAESKey() throws InvalidKeySpecException { + var chars = DataStorage.get() != null ? DataStorage.get().getVaultKey().toCharArray() : new char[0]; + return getSecretKey(chars); + } + + @Override + public InPlaceSecretValue inPlace() { + return new InPlaceSecretValue(getSecret()); + } + + @Override + public String toString() { + return ""; + } +} diff --git a/app/src/main/java/io/xpipe/app/util/WindowsRegistry.java b/app/src/main/java/io/xpipe/app/util/WindowsRegistry.java index 1354c754..318e6cba 100644 --- a/app/src/main/java/io/xpipe/app/util/WindowsRegistry.java +++ b/app/src/main/java/io/xpipe/app/util/WindowsRegistry.java @@ -32,7 +32,9 @@ public class WindowsRegistry { // This can fail even with errors in case the jna native library extraction fails try { if (!Advapi32Util.registryValueExists( - hkey == HKEY_LOCAL_MACHINE ? WinReg.HKEY_LOCAL_MACHINE : WinReg.HKEY_CURRENT_USER, key, valueName)) { + hkey == HKEY_LOCAL_MACHINE ? WinReg.HKEY_LOCAL_MACHINE : WinReg.HKEY_CURRENT_USER, + key, + valueName)) { return Optional.empty(); } @@ -44,8 +46,13 @@ public class WindowsRegistry { } } - public static Optional readRemoteString(ShellControl shellControl, int hkey, String key, String valueName) throws Exception { - var command = CommandBuilder.of().add("reg", "query").addQuoted((hkey == HKEY_LOCAL_MACHINE ? "HKEY_LOCAL_MACHINE" : "HKEY_CURRENT_USER") + "\\" + key).add("/v").addQuoted(valueName); + public static Optional readRemoteString(ShellControl shellControl, int hkey, String key, String valueName) + throws Exception { + var command = CommandBuilder.of() + .add("reg", "query") + .addQuoted((hkey == HKEY_LOCAL_MACHINE ? "HKEY_LOCAL_MACHINE" : "HKEY_CURRENT_USER") + "\\" + key) + .add("/v") + .addQuoted(valueName); String output; try (var c = shellControl.command(command).start()) { diff --git a/app/src/main/java/io/xpipe/app/util/XPipeSession.java b/app/src/main/java/io/xpipe/app/util/XPipeSession.java index 4f85b3b0..bde87867 100644 --- a/app/src/main/java/io/xpipe/app/util/XPipeSession.java +++ b/app/src/main/java/io/xpipe/app/util/XPipeSession.java @@ -8,21 +8,17 @@ import java.util.UUID; @Value public class XPipeSession { + private static XPipeSession INSTANCE; boolean isNewBuildSession; - /** * Unique identifier that resets on every XPipe restart. */ UUID sessionId; - /** * Unique identifier that resets on every XPipe update. */ UUID buildSessionId; - - private static XPipeSession INSTANCE; - public static void init(UUID buildSessionId) { if (INSTANCE != null) { return; diff --git a/app/src/main/java/module-info.java b/app/src/main/java/module-info.java index c44e94fb..b5d542e1 100644 --- a/app/src/main/java/module-info.java +++ b/app/src/main/java/module-info.java @@ -3,14 +3,17 @@ import io.xpipe.app.browser.action.BrowserAction; import io.xpipe.app.core.AppLogs; import io.xpipe.app.exchange.*; import io.xpipe.app.exchange.cli.*; -import io.xpipe.app.ext.*; +import io.xpipe.app.ext.ActionProvider; +import io.xpipe.app.ext.DataStoreProvider; +import io.xpipe.app.ext.PrefsProvider; +import io.xpipe.app.ext.ScanProvider; import io.xpipe.app.issue.EventHandler; import io.xpipe.app.issue.EventHandlerImpl; 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 +47,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 +64,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 +74,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 +97,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; @@ -122,7 +111,8 @@ open module io.xpipe.app { uses LicenseProvider; uses io.xpipe.app.util.LicensedFeature; - provides Module with StorageJacksonModule; + provides Module with + StorageJacksonModule; provides ModuleLayerLoader with ActionProvider.Loader, PrefsProvider.Loader, @@ -155,6 +145,8 @@ open module io.xpipe.app { ListStoresExchangeImpl, StoreAddExchangeImpl, 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 00000000..836aa17b --- /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/preferences_en.properties b/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties index bd72e263..202d35c5 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,26 @@ -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 +localShell=Local shell +themeDescription=You preferred theme +dontAutomaticallyStartVmSshServer=Don't automatically start SSH server for VMs when needed +dontAutomaticallyStartVmSshServerDescription=Any shell connection to a VM running in a hypervisor is made through SSH. XPipe can automatically start the installed SSH server when needed. If you don't want this for security reasons, then you can just disable this behavior with this option. 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 +39,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 +55,15 @@ windowOptions=Window Options saveWindowLocation=Save window location saveWindowLocationDescription=Controls whether the window coordinates should be saved and restored on restarts. startupShutdown=Startup / Shutdown +showChildCategoriesInParentCategory=Show child categories in parent category +showChildCategoriesInParentCategoryDescription=Whether or not to include all connections located in sub categories when having a certain parent category is selected.\n\nIf this is disabled, the categories behave more like classical folders which only show their direct contents without including sub folders. +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. +openConnectionSearchWindowOnConnectionCreation=Open connection search window on connection creation +openConnectionSearchWindowOnConnectionCreationDescription=Whether or not to automatically open the window to search for available subconnections upon adding a new shell connection. +workflow=Workflow system=System application=Application updateToPrereleases=Include prereleases @@ -90,13 +110,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 +140,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 +159,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 +192,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 df22f20e..a5d6439f 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 @@ -1,3 +1,111 @@ +# Interface +addCollection=Add collection +newCollection=New collection +delete=Delete +rename=Rename +properties=Properties +usedDate=Used $DATE$ +cols=$COLS$ columns +rowsCols=$ROWS$ rows / $COLS$ columns +lines=$LINES$ lines +objects=$OBJECTS$ objects +bytes=$BYTES$ bytes +entries=$ENTRIES$ entries +unknownLength=Unknown length +temporaryCollection=Temporary +entrySettings=Settings +pipe=Pipe +openDir=Open Directory +failedToLoad=Failed to Load +stores=Stores +# Dialogs +confirmCollectionDeletionTitle=Confirm collection deletion +confirmCollectionDeletionHeader=Do you really want to delete the collection $NAME$? +confirmCollectionDeletionContent=This will delete all ($COUNT$) contained data sources as well. +retrieveDataSource=Retrieve Data Source +# Tooltips +addCollectionFolder=Create new collection folder +collectionOptions=Collection options +addStreamDataSource=Add stream data source +addDatabaseDataSource=Add database data source +displayList=Display as list +displayTiles=Display as tiles +sortLastUsed=Sort by last used date +sortAlphabetical=Sort alphabetical by name +temporaryCollectionNote=All data sources inside the temporary collection are only stored while until the next system restart. +storeForLaterUse=Store for later use +localFile=Local File +network=Network +recentFiles=Recent files +newDataSource=New data source +newDataStore=New data store +selectInput=Select Input +configure=Configure +retrieve=Retrieve +internet=Internet +table=Table +update=Update +selectStreamStore=Select Stream Store +openStreamStoreWizard=Open Stream Store Wizard +updateDataSource=Update Data Source +structure=Structure +text=Text +raw=Raw +collection=Collection +anyFile=Any file +anyStream=Any Stream Type +noMatchingStoreFound=No suitable saved store was found +addStore=Add Store +anyStreamDescription=Or choose specific type +restart=Restart XPipe +restartDescription=A restart can often be a quick fix +reportIssue=Report Issue +reportIssueDescription=Open the integrated issue reporter +usefulActions=Useful actions +stored=Saved +troubleshootingOptions=Troubleshooting tools +troubleshoot=Troubleshoot +remote=Remote File +addShellStore=Add Shell ... +addShellTitle=Add Shell Connection +savedConnections=Saved Connections +save=Save +clean=Clean +refresh=Refresh +moveTo=Move to ... +addDatabase=Database ... +browseInternalStorage=Browse internal storage +addTunnel=Tunnel ... +addScript=Script ... +addHost=Remote Host ... +addShell=Shell Environment ... +addCommand=Custom Command ... +addAutomatically=Search Automatically ... +addOther=Add Other ... +addStreamTitle=Add Stream Store +addConnection=Add Connection +skip=Skip +addConnections=New +selectType=Select Type +selectTypeDescription=Select connection type +selectDatabaseType=Database Type +selectDatabaseTypeDescription=Select Type of the Database +selectShellType=Shell Type +selectShellTypeDescription=Select the Type of the Shell Connection +selectStreamType=Stream Type +selectStreamTypeDescription=Select type of the stream +name=Name +storeIntroTitle=Connection Hub +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. +detectConnections=Search for connections +configuration=Configuration +dragAndDropFilesHere=Or just drag and drop a file here +confirmDsCreationAbortTitle=Confirm abort +confirmDsCreationAbortHeader=Do you want to abort the data source creation? +confirmDsCreationAbortContent=Any data source creation progress will be lost. +confirmInvalidStoreTitle=Failed connection +confirmInvalidStoreHeader=Do you want to skip connection validation? +confirmInvalidStoreContent=You can add this connection even if it could not be validated and fix the connection problems later on. charset=Charset newLine=Newline crlf=CRLF (Windows) @@ -7,6 +115,14 @@ common=Common key=Key color=Color roadmap=Roadmap and feature requests +alwaysConfirmElevation=Always confirm elevation +alwaysConfirmElevationDescription=Controls how to handle cases when elevated access is required to run a command on a system, e.g. with sudo.\n\nBy default, any sudo credentials are cached during a session and automatically provided when needed. If this option is enabled, it will ask you to confirm the elevation access every time. +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 +144,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 c15d0c20..8dee7ae2 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 9603bbd9..e67897b1 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 c6c3798f..b7ebe2e9 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 6963da1f..28a74c1e 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/dialog-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/dialog-comp.css new file mode 100644 index 00000000..196a8efa --- /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/header-bars.css b/app/src/main/resources/io/xpipe/app/resources/style/header-bars.css index b7d6a634..661f01e9 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/header-bars.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/header-bars.css @@ -3,7 +3,10 @@ -fx-padding: 0.8em 1.0em 0.8em 1.0em; -fx-background-color: -color-bg-subtle; -fx-border-color: -color-border-default; --fx-effect: dropshadow(three-pass-box, -color-shadow-default, 3px, 0.5, 0, 1); +} + +.root:pretty .bar { + -fx-effect: dropshadow(three-pass-box, -color-shadow-default, 3px, 0.5, 0, 1); } .store-header-bar { @@ -102,10 +105,8 @@ -fx-padding: 0.8em 0.5em 0.8em 0.5em; -fx-background-color: -color-bg-subtle; -fx-border-color: -color-border-default; --fx-effect: dropshadow(three-pass-box, -color-shadow-default, 3px, 0.5, 0, 1); } - .store-sort-bar { -fx-background-radius: 0 4px 4px 0; -fx-border-radius: 0 4px 4px 0; 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 c89e7598..d4367d0c 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 c0e3564f..f75082ca 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/prefs.css b/app/src/main/resources/io/xpipe/app/resources/style/prefs.css index 3e49e535..e54008b6 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 1a6d8de9..1e1916f8 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; @@ -19,4 +14,8 @@ .options-comp .description { -fx-opacity: 0.75; +} + +.options-comp .long-description { + -fx-padding: 0 6 0 6; } \ No newline at end of file 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 1502ae6c..619fec83 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-entry-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/store-entry-comp.css index 430ff650..7731818a 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 37de0101..07ce369c 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/test/java/Test.java b/app/src/test/java/Test.java new file mode 100644 index 00000000..d5ac1298 --- /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 8b14e6f7..0d8e4d3f 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" @@ -20,6 +19,9 @@ repositories { } dependencies { + compileOnly 'org.hamcrest:hamcrest:2.2' + compileOnly 'org.junit.jupiter:junit-jupiter-api:5.10.2' + compileOnly 'org.junit.jupiter:junit-jupiter-params:5.10.2' api project(':core') } diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java index 4329147c..bcef6b4a 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java @@ -81,6 +81,17 @@ public class BeaconClient implements AutoCloseable { // }) // } + @Override + public void close() throws ConnectorException { + try { + getRawInputStream().readAllBytes(); + } catch (IOException ex) { + throw new ConnectorException(ex); + } + + super.close(); + } + @Override public T receiveResponse() throws ConnectorException, ClientException, ServerException { @@ -93,17 +104,6 @@ public class BeaconClient implements AutoCloseable { return super.receiveResponse(); } - - @Override - public void close() throws ConnectorException { - try { - getRawInputStream().readAllBytes(); - } catch (IOException ex) { - throw new ConnectorException(ex); - } - - super.close(); - } }; } @@ -314,7 +314,6 @@ public class BeaconClient implements AutoCloseable { } } - @JsonTypeName("daemon") @Value @Builder diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconConfig.java b/beacon/src/main/java/io/xpipe/beacon/BeaconConfig.java index 2e858e39..9b7113f8 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconConfig.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconConfig.java @@ -10,12 +10,12 @@ public class BeaconConfig { public static final byte[] BODY_SEPARATOR = "\n\n".getBytes(StandardCharsets.UTF_8); public static final String BEACON_PORT_PROP = "io.xpipe.beacon.port"; + public static final String DAEMON_ARGUMENTS_PROP = "io.xpipe.beacon.daemonArgs"; private static final String PRINT_MESSAGES_PROPERTY = "io.xpipe.beacon.printMessages"; private static final String LAUNCH_DAEMON_IN_DEBUG_PROP = "io.xpipe.beacon.launchDebugDaemon"; private static final String ATTACH_DEBUGGER_PROP = "io.xpipe.beacon.attachDebuggerToDaemon"; private static final String EXEC_DEBUG_PROP = "io.xpipe.beacon.printDaemonOutput"; private static final String EXEC_PROCESS_PROP = "io.xpipe.beacon.customDaemonCommand"; - public static final String DAEMON_ARGUMENTS_PROP = "io.xpipe.beacon.daemonArgs"; private static final String LOCAL_PROXY_PROP = "io.xpipe.beacon.localProxy"; public static boolean localProxy() { diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconFormat.java b/beacon/src/main/java/io/xpipe/beacon/BeaconFormat.java index 1ca9f29f..029f2305 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconFormat.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconFormat.java @@ -17,17 +17,6 @@ public class BeaconFormat { private final byte[] currentBytes = new byte[SEGMENT_SIZE]; private int index; - @Override - public void close() throws IOException { - if (isClosed()) { - return; - } - - finishBlock(); - out.flush(); - index = -1; - } - @Override public void write(int b) throws IOException { if (isClosed()) { @@ -42,6 +31,17 @@ public class BeaconFormat { index++; } + @Override + public void close() throws IOException { + if (isClosed()) { + return; + } + + finishBlock(); + out.flush(); + index = -1; + } + private boolean isClosed() { return index == -1; } diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java b/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java index 0d245155..b151ef9b 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java @@ -1,8 +1,8 @@ package io.xpipe.beacon; import io.xpipe.beacon.exchange.StopExchange; -import io.xpipe.core.store.FileNames; import io.xpipe.core.process.OsType; +import io.xpipe.core.store.FileNames; import io.xpipe.core.util.XPipeDaemonMode; import io.xpipe.core.util.XPipeInstallation; 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 581b82e1..6e533447 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/DrainExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/DrainExchange.java index 6879dd36..93621749 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/DrainExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/DrainExchange.java @@ -29,6 +29,5 @@ public class DrainExchange implements MessageExchange { @Jacksonized @Builder @Value - public static class Response implements ResponseMessage { - } + public static class Response implements ResponseMessage {} } diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/SinkExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/SinkExchange.java index 1bcce0dd..f79ff2b7 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/SinkExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/SinkExchange.java @@ -29,6 +29,5 @@ public class SinkExchange implements MessageExchange { @Jacksonized @Builder @Value - public static class Response implements ResponseMessage { - } + public static class Response implements ResponseMessage {} } 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 00000000..0f4571fc --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/TerminalLaunchExchange.java @@ -0,0 +1,35 @@ +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 00000000..7dab79a9 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/TerminalWaitExchange.java @@ -0,0 +1,31 @@ +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/io/xpipe/beacon/test/BeaconDaemonController.java b/beacon/src/main/java/io/xpipe/beacon/test/BeaconDaemonController.java new file mode 100644 index 00000000..a5569604 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/test/BeaconDaemonController.java @@ -0,0 +1,95 @@ +package io.xpipe.beacon.test; + +import io.xpipe.beacon.BeaconClient; +import io.xpipe.beacon.BeaconServer; +import io.xpipe.core.util.XPipeDaemonMode; +import io.xpipe.core.util.XPipeInstallation; + +import java.io.IOException; + +public class BeaconDaemonController { + + private static boolean alreadyStarted; + + public static void start(XPipeDaemonMode mode) throws Exception { + if (BeaconServer.isReachable()) { + alreadyStarted = true; + return; + } + + var custom = false; + Process process; + if ((process = BeaconServer.tryStartCustom()) != null) { + custom = true; + } else { + var defaultBase = XPipeInstallation.getLocalDefaultInstallationBasePath(); + process = BeaconServer.start(defaultBase, mode); + } + + waitForStartup(process, custom); + if (!BeaconServer.isReachable()) { + throw new AssertionError(); + } + } + + public static void stop() throws Exception { + if (alreadyStarted) { + return; + } + + if (!BeaconServer.isReachable()) { + return; + } + + var client = BeaconClient.establishConnection(BeaconClient.ApiClientInformation.builder() + .version("?") + .language("Java API Test") + .build()); + if (!BeaconServer.tryStop(client)) { + throw new AssertionError(); + } + waitForShutdown(); + } + + private static void waitForStartup(Process process, boolean custom) throws IOException { + for (int i = 0; i < 160; i++) { + // Breaks when using nohup & disown + // if (process != null && !custom && !process.isAlive()) { + // throw new IOException("Daemon start failed"); + // } + + if (process != null && custom && !process.isAlive() && process.exitValue() != 0) { + throw new IOException("Custom launch command failed"); + } + + try { + Thread.sleep(500); + } catch (InterruptedException ignored) { + } + + var s = BeaconClient.tryEstablishConnection(BeaconClient.ApiClientInformation.builder() + .version("?") + .language("Java") + .build()); + if (s.isPresent()) { + return; + } + } + + throw new IOException("Wait for daemon start up timed out"); + } + + private static void waitForShutdown() { + for (int i = 0; i < 40; i++) { + try { + Thread.sleep(500); + } catch (InterruptedException ignored) { + } + + var r = BeaconServer.isReachable(); + if (!r) { + return; + } + } + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/test/BeaconDaemonExtensionTest.java b/beacon/src/main/java/io/xpipe/beacon/test/BeaconDaemonExtensionTest.java new file mode 100644 index 00000000..9fae1ee7 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/test/BeaconDaemonExtensionTest.java @@ -0,0 +1,20 @@ +package io.xpipe.beacon.test; + +import io.xpipe.core.util.JacksonMapper; +import io.xpipe.core.util.XPipeDaemonMode; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; + +public class BeaconDaemonExtensionTest { + + @BeforeAll + public static void setup() throws Exception { + JacksonMapper.initModularized(ModuleLayer.boot()); + BeaconDaemonController.start(XPipeDaemonMode.TRAY); + } + + @AfterAll + public static void teardown() throws Exception { + BeaconDaemonController.stop(); + } +} diff --git a/beacon/src/main/java/module-info.java b/beacon/src/main/java/module-info.java index 7bdb970c..eac7269b 100644 --- a/beacon/src/main/java/module-info.java +++ b/beacon/src/main/java/module-info.java @@ -4,25 +4,19 @@ import io.xpipe.beacon.exchange.*; import io.xpipe.beacon.exchange.cli.*; import io.xpipe.core.util.ProxyFunction; -module io.xpipe.beacon { +open module io.xpipe.beacon { exports io.xpipe.beacon; exports io.xpipe.beacon.exchange; exports io.xpipe.beacon.exchange.data; exports io.xpipe.beacon.exchange.cli; - - opens io.xpipe.beacon; - opens io.xpipe.beacon.exchange; - opens io.xpipe.beacon.exchange.data; - opens io.xpipe.beacon.exchange.cli; - exports io.xpipe.beacon.util; - - opens io.xpipe.beacon.util; + exports io.xpipe.beacon.test; requires static com.fasterxml.jackson.core; requires static com.fasterxml.jackson.databind; requires transitive io.xpipe.core; requires static lombok; + requires static org.junit.jupiter.api; uses MessageExchange; uses ProxyFunction; @@ -49,6 +43,8 @@ module io.xpipe.beacon { StoreAddExchange, ReadDrainExchange, AskpassExchange, + TerminalWaitExchange, + TerminalLaunchExchange, ListStoresExchange, DialogExchange, VersionExchange; diff --git a/build.gradle b/build.gradle index b23abd33..48a7b3ea 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() @@ -33,6 +57,7 @@ project.ext { arch = getArchName() privateExtensions = file("$rootDir/private_extensions.txt").exists() ? file("$rootDir/private_extensions.txt").readLines() : [] isFullRelease = System.getenv('RELEASE') != null && Boolean.parseBoolean(System.getenv('RELEASE')) + isPreRelease = System.getenv('PRERELEASE') != null && Boolean.parseBoolean(System.getenv('PRERELEASE')) isStage = System.getenv('STAGE') != null && Boolean.parseBoolean(System.getenv('STAGE')) rawVersion = file('version').text.trim() versionString = rawVersion + (isFullRelease || isStage ? '' : '-SNAPSHOT') @@ -49,7 +74,35 @@ 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", + "-Dapple.awt.application.appearance=system" + ] + useBundledJavaFx = fullVersion && !(platformName == 'linux' && arch == 'arm64') + announce = System.getenv('SKIP_ANNOUNCEMENT') == null || !Boolean.parseBoolean(System.getenv('SKIP_ANNOUNCEMENT')) + changelogFile = file("$rootDir/dist/changelogs/${versionString}.md").exists() ? + file("$rootDir/dist/changelogs/${versionString}.md") : + file("$rootDir/dist/changelogs/${canonicalVersionString}.md") +} + +if (org.gradle.internal.os.OperatingSystem.current() == org.gradle.internal.os.OperatingSystem.LINUX) { + jvmRunArgs.addAll("--add-opens", "java.desktop/sun.awt.X11=io.xpipe.app") +} +if (org.gradle.internal.os.OperatingSystem.current() == org.gradle.internal.os.OperatingSystem.MAC_OS) { + jvmRunArgs.addAll("--add-exports", "java.desktop/com.apple.eawt=io.xpipe.app") } if (isFullRelease && rawVersion.contains("-")) { @@ -73,3 +126,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().filter {!it.isEmpty()}.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 a0dc3967..a6b64d35 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/charsetter/Charsetter.java b/core/src/main/java/io/xpipe/core/charsetter/Charsetter.java index 0dbf9b47..9779a1bd 100644 --- a/core/src/main/java/io/xpipe/core/charsetter/Charsetter.java +++ b/core/src/main/java/io/xpipe/core/charsetter/Charsetter.java @@ -75,9 +75,7 @@ public abstract class Charsetter { return new BufferedReader(new InputStreamReader(stream, charset.getCharset())); } - public abstract Result read( - FailableSupplier in, FailableConsumer con) - throws Exception; + public abstract Result read(FailableSupplier in, FailableConsumer con); public Result detect(StreamDataStore store) throws Exception { Result result = new Result(null, null); @@ -107,13 +105,13 @@ public abstract class Charsetter { } } -// if (store instanceof FileStore fileStore && fileStore.getFileSystem() instanceof ShellStore m) { -// if (result.getNewLine() == null) { -// result = new Result( -// result.getCharset(), -// m.getShellType() != null ? m.getShellType().getNewLine() : null); -// } -// } + // if (store instanceof FileStore fileStore && fileStore.getFileSystem() instanceof ShellStore m) { + // if (result.getNewLine() == null) { + // result = new Result( + // result.getCharset(), + // m.getShellType() != null ? m.getShellType().getNewLine() : null); + // } + // } if (result.getCharset() == null) { result = new Result(StreamCharset.UTF8, result.getNewLine()); diff --git a/core/src/main/java/io/xpipe/core/charsetter/NewLine.java b/core/src/main/java/io/xpipe/core/charsetter/NewLine.java index 550e044f..04a5804b 100644 --- a/core/src/main/java/io/xpipe/core/charsetter/NewLine.java +++ b/core/src/main/java/io/xpipe/core/charsetter/NewLine.java @@ -12,6 +12,7 @@ public enum NewLine { CRLF("\r\n", "crlf"); private final String newLine; + @Getter private final String id; @@ -22,7 +23,7 @@ public enum NewLine { public static NewLine platform() { return Arrays.stream(values()) - .filter(n -> n.getNewLineString().equals(System.getProperty("line.separator"))) + .filter(n -> n.getNewLineString().equals(System.lineSeparator())) .findFirst() .orElseThrow(); } @@ -37,5 +38,4 @@ public enum NewLine { public String getNewLineString() { return newLine; } - } diff --git a/core/src/main/java/io/xpipe/core/charsetter/StreamCharset.java b/core/src/main/java/io/xpipe/core/charsetter/StreamCharset.java index 828b9013..9de184bc 100644 --- a/core/src/main/java/io/xpipe/core/charsetter/StreamCharset.java +++ b/core/src/main/java/io/xpipe/core/charsetter/StreamCharset.java @@ -120,17 +120,6 @@ public class StreamCharset { byte[] byteOrderMark; List names; - public Reader reader(InputStream stream) throws Exception { - if (hasByteOrderMark()) { - var bom = stream.readNBytes(getByteOrderMark().length); - if (bom.length != 0 && !Arrays.equals(bom, getByteOrderMark())) { - throw new IllegalStateException("Charset does not match: " + charset.toString()); - } - } - - return new InputStreamReader(stream, charset); - } - public static StreamCharset get(Charset charset, boolean byteOrderMark) { return ALL.stream() .filter(streamCharset -> @@ -150,6 +139,24 @@ public class StreamCharset { return found.get(); } + public Reader reader(InputStream stream) throws Exception { + if (hasByteOrderMark()) { + var bom = stream.readNBytes(getByteOrderMark().length); + if (bom.length != 0 && !Arrays.equals(bom, getByteOrderMark())) { + throw new IllegalStateException("Charset does not match: " + charset.toString()); + } + } + + return new InputStreamReader(stream, charset); + } + + @Override + public int hashCode() { + int result = Objects.hash(charset); + result = 31 * result + Arrays.hashCode(byteOrderMark); + return result; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -161,15 +168,8 @@ public class StreamCharset { return charset.equals(that.charset) && Arrays.equals(byteOrderMark, that.byteOrderMark); } - @Override - public int hashCode() { - int result = Objects.hash(charset); - result = 31 * result + Arrays.hashCode(byteOrderMark); - return result; - } - public String toString() { - return getNames().get(0); + return getNames().getFirst(); } public boolean hasByteOrderMark() { diff --git a/core/src/main/java/io/xpipe/core/dialog/BaseQueryElement.java b/core/src/main/java/io/xpipe/core/dialog/BaseQueryElement.java index b2c41c92..f6f14b91 100644 --- a/core/src/main/java/io/xpipe/core/dialog/BaseQueryElement.java +++ b/core/src/main/java/io/xpipe/core/dialog/BaseQueryElement.java @@ -13,8 +13,10 @@ import lombok.ToString; public class BaseQueryElement extends DialogElement { private final String description; + @Getter private final boolean newLine; + private final boolean required; private final boolean secret; private final boolean quiet; @@ -31,13 +33,13 @@ public class BaseQueryElement extends DialogElement { this.value = value; } - @Override - public boolean requiresExplicitUserInput() { - return required && value == null; - } - @Override public String toDisplayString() { return description; } + + @Override + public boolean requiresExplicitUserInput() { + return required && value == null; + } } diff --git a/core/src/main/java/io/xpipe/core/dialog/ChoiceElement.java b/core/src/main/java/io/xpipe/core/dialog/ChoiceElement.java index 9f87a658..2e37ad6b 100644 --- a/core/src/main/java/io/xpipe/core/dialog/ChoiceElement.java +++ b/core/src/main/java/io/xpipe/core/dialog/ChoiceElement.java @@ -16,10 +16,13 @@ public class ChoiceElement extends DialogElement { @Getter private final String description; + @Getter private final List elements; + @Getter private final boolean required; + private final boolean quiet; @Getter @@ -39,13 +42,13 @@ public class ChoiceElement extends DialogElement { } @Override - public boolean requiresExplicitUserInput() { - return required && selected == -1; + public String toDisplayString() { + return description; } @Override - public String toDisplayString() { - return description; + public boolean requiresExplicitUserInput() { + return required && selected == -1; } @Override @@ -79,5 +82,4 @@ public class ChoiceElement extends DialogElement { return false; } - } diff --git a/core/src/main/java/io/xpipe/core/dialog/Dialog.java b/core/src/main/java/io/xpipe/core/dialog/Dialog.java index 5299dce7..7d88c80b 100644 --- a/core/src/main/java/io/xpipe/core/dialog/Dialog.java +++ b/core/src/main/java/io/xpipe/core/dialog/Dialog.java @@ -1,6 +1,5 @@ package io.xpipe.core.dialog; -import io.xpipe.core.charsetter.Charsetter; import io.xpipe.core.util.FailableConsumer; import io.xpipe.core.util.FailableSupplier; import io.xpipe.core.util.SecretValue; @@ -24,7 +23,7 @@ import java.util.function.Supplier; * The evaluation function can be set with {@link #evaluateTo(Supplier)}. * Alternatively, a dialogue can also copy the evaluation function of another dialogue with {@link #evaluateTo(Dialog)}. * An evaluation result can also be mapped to another type with {@link #map(Function)}. - * It is also possible to listen for the completion of this dialogue with {@link #onCompletion(Charsetter.FailableConsumer)} )}. + * It is also possible to listen for the completion of this dialogue with {@link #onCompletion(FailableConsumer)}. */ public abstract class Dialog { @@ -76,7 +75,6 @@ public abstract class Dialog { * @param description the shown question description * @param toString a function that maps the objects to a string * @param required signals whether choices required or can be left empty - * @param quiet * @param def the element which is selected by default * @param vals the range of possible elements */ diff --git a/core/src/main/java/io/xpipe/core/dialog/DialogElement.java b/core/src/main/java/io/xpipe/core/dialog/DialogElement.java index aca75582..97a643be 100644 --- a/core/src/main/java/io/xpipe/core/dialog/DialogElement.java +++ b/core/src/main/java/io/xpipe/core/dialog/DialogElement.java @@ -28,5 +28,4 @@ public abstract class DialogElement { public boolean apply(String value) { throw new UnsupportedOperationException(); } - } diff --git a/core/src/main/java/io/xpipe/core/dialog/HeaderElement.java b/core/src/main/java/io/xpipe/core/dialog/HeaderElement.java index 80870ead..55e9e8fd 100644 --- a/core/src/main/java/io/xpipe/core/dialog/HeaderElement.java +++ b/core/src/main/java/io/xpipe/core/dialog/HeaderElement.java @@ -28,5 +28,4 @@ public class HeaderElement extends DialogElement { public boolean apply(String value) { return true; } - } 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 819dbc74..f41c77cd 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 00000000..4af32703 --- /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 146c071f..3fb6b6be 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; @@ -8,15 +10,37 @@ import java.util.function.Function; public class CommandBuilder { + private final List elements = new ArrayList<>(); + + @Getter + private final Map environmentVariables = new LinkedHashMap<>(); + + private final List> setup = new ArrayList<>(); + + @Getter + private CountDown countDown; + + @Getter + private UUID uuid; + + private CommandBuilder() {} + public static CommandBuilder of() { return new CommandBuilder(); } - private CommandBuilder() {} + public static CommandBuilder ofString(String s) { + return new CommandBuilder().add(s); + } - private final List elements = new ArrayList<>(); - @Getter - private final Map environmentVariables = new LinkedHashMap<>(); + public static CommandBuilder ofFunction(FailableFunction command) { + return CommandBuilder.of().add(sc -> command.apply(sc)); + } + + public CommandBuilder setup(FailableConsumer consumer) { + setup.add(consumer); + return this; + } public CommandBuilder fixedEnvrironment(String k, String v) { environmentVariables.put(k, new Fixed(v)); @@ -38,25 +62,6 @@ public class CommandBuilder { return this; } - public interface Element { - - String evaluate(ShellControl sc) throws Exception; - } - - static class Fixed implements Element { - - private final String string; - - Fixed(String string) { - this.string = string; - } - - @Override - public String evaluate(ShellControl sc) { - return string; - } - } - public CommandBuilder discardOutput() { elements.add(sc -> sc.getShellDialect().getDiscardOperator()); return this; @@ -83,7 +88,6 @@ public class CommandBuilder { return this; } - public CommandBuilder add(int index, String... s) { for (String s1 : s) { elements.add(index++, new Fixed(s1)); @@ -126,19 +130,14 @@ 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; } public CommandBuilder prepend(Element e) { - elements.add(0, e); + elements.addFirst(e); return this; } @@ -156,21 +155,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 +170,49 @@ 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 { + countDown = CountDown.of(); + uuid = UUID.randomUUID(); + + for (FailableConsumer s : setup) { + s.accept(sc); + } + List list = new ArrayList<>(); for (Element element : elements) { String evaluate = element.evaluate(sc); @@ -206,7 +226,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); @@ -234,4 +254,23 @@ public class CommandBuilder { } return String.join(" ", list); } + + public interface Element { + + String evaluate(ShellControl sc) throws Exception; + } + + static class Fixed implements Element { + + private final String string; + + Fixed(String string) { + this.string = string; + } + + @Override + public String evaluate(ShellControl sc) { + return string; + } + } } 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 06aa1877..37f4fba0 100644 --- a/core/src/main/java/io/xpipe/core/process/CommandControl.java +++ b/core/src/main/java/io/xpipe/core/process/CommandControl.java @@ -21,15 +21,13 @@ public interface CommandControl extends ProcessControl { int INTERNAL_ERROR_EXIT_CODE = 163; int ELEVATION_FAILED_EXIT_CODE = 164; - enum TerminalExitMode { - KEEP_OPEN, - CLOSE - } - void setSensitive(); CommandControl withExceptionConverter(ExceptionConverter converter); + @Override + CommandControl start() throws Exception; + CommandControl withErrorFormatter(Function formatter); CommandControl terminalExitMode(TerminalExitMode mode); @@ -72,15 +70,12 @@ public interface CommandControl extends ProcessControl { CommandControl elevated(String message, FailableFunction elevationFunction); - @Override - CommandControl start() throws Exception; - - CommandControl exitTimeout(Integer timeout); - void withStdoutOrThrow(FailableConsumer c); String readStdoutDiscardErr() throws Exception; + String readJoinedOutputOrThrow() throws Exception; + String readStderrDiscardStdout() throws Exception; void discardOrThrow() throws Exception; @@ -115,4 +110,9 @@ public interface CommandControl extends ProcessControl { void discardOut(); void discardErr(); + + enum TerminalExitMode { + KEEP_OPEN, + CLOSE + } } 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 00000000..59d579f6 --- /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 { + + private volatile long lastMillis = -1; + private volatile long millisecondsLeft; + + @Setter + private volatile boolean active; + + @Getter + private volatile long maxMillis; + + private CountDown() {} + + public static CountDown of() { + return new 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 3ad5e099..30992b95 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,15 @@ 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 { 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 +42,17 @@ public sealed interface OsType permits OsType.Windows, OsType.Linux, OsType.MacO String determineOperatingSystemName(ShellControl pc) throws Exception; - final class Windows implements 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 {} + + final class Windows implements OsType, Local, Any { @Override public List determineInterestingPaths(ShellControl pc) throws Exception { @@ -102,7 +114,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 { @@ -122,13 +134,13 @@ public sealed interface OsType permits OsType.Windows, OsType.Linux, OsType.MacO } @Override - public String getTempDirectory(ShellControl pc) { - return "/tmp/"; + public String getName() { + return "Linux"; } @Override - public String getName() { - return "Linux"; + public String getTempDirectory(ShellControl pc) { + return "/tmp/"; } @Override @@ -138,20 +150,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 +170,33 @@ 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 { @@ -193,6 +217,16 @@ public sealed interface OsType permits OsType.Windows, OsType.Linux, OsType.MacO return pc.executeSimpleStringCommand(pc.getShellDialect().getPrintEnvironmentVariableCommand("HOME")); } + @Override + public String getFileSystemSeparator() { + return "/"; + } + + @Override + public String getName() { + return "Mac"; + } + @Override public String getTempDirectory(ShellControl pc) throws Exception { var found = pc.executeSimpleStringCommand(pc.getShellDialect().getPrintVariableCommand("TMPDIR")); @@ -205,20 +239,9 @@ public sealed interface OsType permits OsType.Windows, OsType.Linux, OsType.MacO return found; } - @Override - public String getFileSystemSeparator() { - return "/"; - } - - @Override - public String getName() { - return "Mac"; - } - @Override public Map getProperties(ShellControl pc) throws Exception { - try (CommandControl c = - pc.command("sw_vers").start()) { + try (CommandControl c = pc.command("sw_vers").start()) { var text = c.readStdoutOrThrow(); return PropertiesFormatsParser.parse(text, ":"); } 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 00000000..a2810830 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/process/ParentSystemAccess.java @@ -0,0 +1,107 @@ +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 boolean supportsExecutableEnvironment() { + 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 boolean supportsExecutableEnvironment() { + 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 boolean supportsExecutableEnvironment() { + return a1.supportsExecutableEnvironment() && a2.supportsExecutableEnvironment(); + } + + @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(); + + boolean supportsExecutableEnvironment(); + + 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 af04754c..d683fd08 100644 --- a/core/src/main/java/io/xpipe/core/process/ProcessControl.java +++ b/core/src/main/java/io/xpipe/core/process/ProcessControl.java @@ -1,22 +1,24 @@ package io.xpipe.core.process; +import io.xpipe.core.util.FailableFunction; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.Charset; +import java.util.UUID; public interface ProcessControl extends AutoCloseable { - @FunctionalInterface - interface ExceptionConverter { - T convert(T t); - } + UUID getUuid(); 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; @@ -46,4 +48,9 @@ public interface ProcessControl extends AutoCloseable { InputStream getStderr(); Charset getCharset(); + + @FunctionalInterface + interface ExceptionConverter { + T convert(T t); + } } 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 718f72be..dd7f2a51 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; @@ -11,7 +10,9 @@ public abstract class ProcessControlProvider { public static void init(ModuleLayer layer) { INSTANCE = ServiceLoader.load(layer, ProcessControlProvider.class).stream() - .map(localProcessControlProviderProvider -> localProcessControlProviderProvider.get()).findFirst().orElseThrow(); + .map(localProcessControlProviderProvider -> localProcessControlProviderProvider.get()) + .findFirst() + .orElseThrow(); } public static ProcessControlProvider get() { @@ -21,16 +22,19 @@ public abstract class ProcessControlProvider { public abstract ShellControl withDefaultScripts(ShellControl pc); public abstract ShellControl sub( - ShellControl parent, - @NonNull FailableFunction commandFunction, - ShellControl.TerminalOpenFunction terminalCommand); + ShellControl parent, @NonNull ShellOpenFunction commandFunction, ShellOpenFunction terminalCommand); - public abstract CommandControl command( - ShellControl parent, - @NonNull FailableFunction command, - FailableFunction terminalCommand); + public abstract CommandControl command(ShellControl parent, CommandBuilder command, CommandBuilder terminalCommand); public abstract ShellControl createLocalProcessControl(boolean stoppable); - public abstract Object createStorageHandler(); + public abstract Object getGitStorageHandler(); + + 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/ProcessOutputException.java b/core/src/main/java/io/xpipe/core/process/ProcessOutputException.java index 828c5e2e..171aa927 100644 --- a/core/src/main/java/io/xpipe/core/process/ProcessOutputException.java +++ b/core/src/main/java/io/xpipe/core/process/ProcessOutputException.java @@ -8,9 +8,18 @@ import java.util.stream.Collectors; @Getter public class ProcessOutputException extends Exception { + private final long exitCode; + private final String output; + + private ProcessOutputException(String message, long exitCode, String output) { + super(message); + this.exitCode = exitCode; + this.output = output; + } + public static ProcessOutputException withParagraph(String customPrefix, ProcessOutputException ex) { var messageSuffix = ex.getOutput() != null ? ex.getOutput() : ""; - var message = customPrefix + "\n\n" + messageSuffix; + var message = customPrefix + "\n\n" + messageSuffix; return new ProcessOutputException(message, ex.getExitCode(), ex.getOutput()); } @@ -34,7 +43,9 @@ public class ProcessOutputException extends Exception { .START_FAILED_EXIT_CODE -> "Process did not start up properly and had to be killed" + errorSuffix; case CommandControl.EXIT_TIMEOUT_EXIT_CODE -> "Wait for process exit timed out" + errorSuffix; - case CommandControl.UNASSIGNED_EXIT_CODE -> "Process exited with unknown state. Did an external process interfere?" + errorSuffix; + case CommandControl + .UNASSIGNED_EXIT_CODE -> "Process exited with unknown state. Did an external process interfere?" + + errorSuffix; case CommandControl.INTERNAL_ERROR_EXIT_CODE -> "Process execution failed" + errorSuffix; case CommandControl.ELEVATION_FAILED_EXIT_CODE -> "Process elevation failed" + errorSuffix; default -> "Process returned exit code " + exitCode + errorSuffix; @@ -42,17 +53,12 @@ public class ProcessOutputException extends Exception { return new ProcessOutputException(message, exitCode, combinedError); } - private final long exitCode; - private final String output; - - private ProcessOutputException(String message, long exitCode, String output) { - super(message); - this.exitCode = exitCode; - this.output = output; - } - public boolean isIrregularExit() { - return exitCode == CommandControl.EXIT_TIMEOUT_EXIT_CODE || exitCode == CommandControl.START_FAILED_EXIT_CODE || exitCode == CommandControl.UNASSIGNED_EXIT_CODE || exitCode == CommandControl.INTERNAL_ERROR_EXIT_CODE || exitCode == CommandControl.ELEVATION_FAILED_EXIT_CODE; + return exitCode == CommandControl.EXIT_TIMEOUT_EXIT_CODE + || exitCode == CommandControl.START_FAILED_EXIT_CODE + || exitCode == CommandControl.UNASSIGNED_EXIT_CODE + || exitCode == CommandControl.INTERNAL_ERROR_EXIT_CODE + || exitCode == CommandControl.ELEVATION_FAILED_EXIT_CODE; } public boolean isKill() { diff --git a/core/src/main/java/io/xpipe/core/process/ScriptSnippet.java b/core/src/main/java/io/xpipe/core/process/ScriptSnippet.java index 5d99a0e4..ee2f6023 100644 --- a/core/src/main/java/io/xpipe/core/process/ScriptSnippet.java +++ b/core/src/main/java/io/xpipe/core/process/ScriptSnippet.java @@ -5,8 +5,12 @@ import lombok.Getter; public interface ScriptSnippet { + String content(ShellControl shellControl); + + ExecutionType executionType(); + @Getter - public static enum ExecutionType { + enum ExecutionType { @JsonProperty("dumbOnly") DUMB_ONLY("dumbOnly"), @JsonProperty("terminalOnly") @@ -28,8 +32,4 @@ public interface ScriptSnippet { return this == TERMINAL_ONLY || this == BOTH; } } - - String content(ShellControl shellControl); - - ExecutionType executionType(); } 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 17678db0..79ba1974 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,8 @@ import java.util.function.Function; public interface ShellControl extends ProcessControl { + UUID getElevationSecretId(); + List getExitUuids(); Optional getSourceStore(); @@ -24,18 +26,14 @@ public interface ShellControl extends ProcessControl { List getInitCommands(); - ShellControl withTargetTerminalShellDialect(ShellDialect d); + ParentSystemAccess getParentSystemAccess(); - ShellDialect getTargetTerminalShellDialect(); + void setParentSystemAccess(ParentSystemAccess access); - 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); + ShellDialect getOriginalShellDialect(); - ShellControl onPreInit(FailableConsumer pc); + void setOriginalShellDialect(ShellDialect dialect); + + 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); @@ -77,18 +75,19 @@ public interface ShellControl extends ProcessControl { ShellControl withExceptionConverter(ExceptionConverter converter); + @Override + ShellControl start() throws Exception; + 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,52 +143,63 @@ 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); } } - default ShellControl identicalSubShell() { - return subShell(p -> p.getShellDialect().getLoginOpenCommand(), - (sc) -> sc.getShellDialect().getLoginOpenCommand() - ).elevationPassword(getElevationPassword()); + ShellControl withSecurityPolicy(ShellSecurityPolicy policy); + + ShellSecurityPolicy getEffectiveSecurityPolicy(); + + String buildElevatedCommand(CommandConfiguration input, String prefix, UUID requestId, 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() { + return CommandBuilder.of().add(sc -> type.getLoginOpenCommand(sc)); + } + + @Override + public CommandBuilder prepareWithInitCommand(@NonNull String command) { + return CommandBuilder.ofString(command); + } + }; + var s = singularSubShell(o); + s.setParentSystemAccess(ParentSystemAccess.identity()); + return s; } - default T enforceDialect(@NonNull ShellDialect type, FailableFunction sc) throws Exception { + default ShellControl identicalSubShell() { + var o = new ShellOpenFunction() { + + @Override + public CommandBuilder prepareWithoutInitCommand() { + return CommandBuilder.of().add(sc -> sc.getShellDialect().getLoginOpenCommand(sc)); + } + + @Override + public CommandBuilder prepareWithInitCommand(@NonNull String command) { + return CommandBuilder.ofString(command); + } + }; + return singularSubShell(o); + } + + default T enforceDialect(@NonNull ShellDialect type, FailableFunction sc) + throws Exception { if (isRunning() && getShellDialect().equals(type)) { return sc.apply(this); } else { @@ -199,8 +209,9 @@ public interface ShellControl extends ProcessControl { } } - ShellControl subShell( - FailableFunction command, TerminalOpenFunction terminalCommand); + ShellControl subShell(ShellOpenFunction command, ShellOpenFunction terminalCommand); + + ShellControl singularSubShell(ShellOpenFunction command); void writeLineAndReadEcho(String command) throws Exception; @@ -208,34 +219,19 @@ public interface ShellControl extends ProcessControl { void cd(String directory) throws Exception; - @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 cd9d9b8f..1befc59e 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,23 @@ 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(" ")); + default String getLicenseFeatureId() { + return null; } + String terminalLauncherScript(UUID request, String name); + String getExecutableName(); - default boolean isSupportedShell() { - return true; - } - default boolean isSelectable() { return true; } @@ -40,14 +31,12 @@ public interface ShellDialect { return other.equals(this); } - default ShellDialect getDumbReplacementDialect(ShellControl parent) { - return this; - } - String getCatchAllVariable(); String queryVersion(ShellControl shellControl) throws Exception; + CommandControl queryFileSize(ShellControl shellControl, String file); + CommandControl prepareUserTempDirectory(ShellControl shellControl, String directory); String initFileName(ShellControl sc) throws Exception; @@ -58,17 +47,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 entries, boolean append); default String applyRcFileCommand() { return null; @@ -80,7 +67,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 +85,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 +93,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 +101,11 @@ public interface ShellDialect { String getDiscardOperator(); - default String getOrConcatenationOperator() { - return "||"; - } + String nullStdin(String command); 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 +125,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 +139,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 +167,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 00000000..70e7eb76 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/process/ShellDialectAskpass.java @@ -0,0 +1,22 @@ +package io.xpipe.core.process; + +import java.util.List; +import java.util.UUID; + +public interface ShellDialectAskpass { + + String prepareStderrPassthroughContent(ShellControl sc, UUID requestId, String prefix); + + String prepareFixedContent(ShellControl sc, String fileName, List s) throws Exception; + + String elevateDumbCommand( + ShellControl shellControl, + CommandConfiguration command, + UUID requestId, + CountDown countDown, + String message) + throws Exception; + + String elevateTerminalCommandWithPreparedAskpass(ShellControl shellControl, 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 1cf9fbd4..66514d6e 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; @@ -24,10 +24,32 @@ public class ShellDialects { public static ShellDialect FISH; public static ShellDialect CISCO; + public static ShellDialect MIKROTIK; 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(); + } + + private static ShellDialect byId(String name) { + return ALL.stream() + .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 byName(String name) { + return byNameIfPresent(name).orElseThrow(); + } + + public static Optional byNameIfPresent(String name) { + return ALL.stream().filter(shellType -> shellType.getId().equals(name)).findFirst(); } public static class Loader implements ModuleLayerLoader { @@ -50,8 +72,8 @@ public class ShellDialects { CSH = byId("csh"); ASH = byId("ash"); SH = byId("sh"); - SH_BSD = byId("shBsd"); CISCO = byId("cisco"); + MIKROTIK = byId("mikrotik"); RBASH = byId("rbash"); } @@ -65,21 +87,4 @@ public class ShellDialects { return true; } } - - private static ShellDialect byId(String name) { - return ALL.stream() - .filter(shellType -> shellType.getId().equals(name)) - .findFirst() - .orElseThrow(); - } - - public static ShellDialect getPlatformDefault() { - if (OsType.getLocal().equals(OsType.WINDOWS)) { - return CMD; - } else if (OsType.getLocal().equals(OsType.LINUX)) { - return BASH; - } else { - return ZSH; - } - } } 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 00000000..20573ed7 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/process/ShellDumbMode.java @@ -0,0 +1,49 @@ +package io.xpipe.core.process; + +import java.io.IOException; + +public interface ShellDumbMode { + + default boolean supportsAnyPossibleInteraction() { + return true; + } + + default ShellDialect getSwitchDialect() { + 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 Unsupported implements ShellDumbMode { + + @Override + public boolean supportsAnyPossibleInteraction() { + return false; + } + + @Override + public CommandBuilder prepareInlineDumbCommand( + ShellControl self, ShellControl parent, ShellOpenFunction function) { + throw new UnsupportedOperationException(); + } + + @Override + public void prepareDumbInit(ShellControl shellControl) { + throw new UnsupportedOperationException(); + } + + @Override + public void prepareDumbExit(ShellControl shellControl) { + shellControl.kill(); + } + } +} 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 00000000..8f323640 --- /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() { + return CommandBuilder.of().add(b); + } + + @Override + public CommandBuilder prepareWithInitCommand(@NonNull String command) { + throw new UnsupportedOperationException(); + } + }; + } + + static ShellOpenFunction of(CommandBuilder b) { + return new ShellOpenFunction() { + @Override + public CommandBuilder prepareWithoutInitCommand() { + return b; + } + + @Override + public CommandBuilder prepareWithInitCommand(@NonNull String command) { + 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 00000000..6d378a99 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/process/ShellSecurityPolicy.java @@ -0,0 +1,6 @@ +package io.xpipe.core.process; + +public interface ShellSecurityPolicy { + + 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 3857d805..3b45daaa 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/process/SimpleScriptSnippet.java b/core/src/main/java/io/xpipe/core/process/SimpleScriptSnippet.java index 21472125..b1122a5b 100644 --- a/core/src/main/java/io/xpipe/core/process/SimpleScriptSnippet.java +++ b/core/src/main/java/io/xpipe/core/process/SimpleScriptSnippet.java @@ -6,6 +6,7 @@ public class SimpleScriptSnippet implements ScriptSnippet { @NonNull private final String content; + @NonNull private final ExecutionType executionType; diff --git a/core/src/main/java/io/xpipe/core/process/TerminalInitScriptConfig.java b/core/src/main/java/io/xpipe/core/process/TerminalInitScriptConfig.java index 0e8db9bb..ce90b69f 100644 --- a/core/src/main/java/io/xpipe/core/process/TerminalInitScriptConfig.java +++ b/core/src/main/java/io/xpipe/core/process/TerminalInitScriptConfig.java @@ -5,11 +5,11 @@ import lombok.Value; @Value public class TerminalInitScriptConfig { - public static TerminalInitScriptConfig ofName(String name) { - return new TerminalInitScriptConfig(name, true, false); - } - String displayName; boolean clearScreen; boolean hasColor; + + public static TerminalInitScriptConfig ofName(String name) { + return new TerminalInitScriptConfig(name, true, false); + } } 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 c781f36f..7290cdd0 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; @@ -25,26 +26,8 @@ public class ConnectionFileSystem implements FileSystem { } @Override - public List listRoots() throws Exception { - return shellControl.getShellDialect().listRoots(shellControl).toList(); - } - - @Override - public boolean directoryExists(String file) throws Exception { - return shellControl - .getShellDialect() - .directoryExists(shellControl, file) - .executeAndCheck(); - } - - @Override - public void directoryAccessible(String file) throws Exception { - shellControl.executeSimpleCommand(shellControl.getShellDialect().getCdCommand(file)); - } - - @Override - public Stream listFiles(String file) throws Exception { - return shellControl.getShellDialect().listFiles(this, shellControl, file); + public long getFileSize(String file) throws Exception { + return Long.parseLong(shellControl.getShellDialect().queryFileSize(shellControl, file).readStdoutOrThrow()); } @Override @@ -60,6 +43,10 @@ public class ConnectionFileSystem implements FileSystem { @Override public FileSystem open() throws Exception { shellControl.start(); + if (!shellControl.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) { + shellControl.close(); + throw new UnsupportedOperationException("System shell does not support file system interaction"); + } return this; } @@ -72,10 +59,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 +109,8 @@ 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(); } @@ -148,6 +136,29 @@ public class ConnectionFileSystem implements FileSystem { } } + @Override + public boolean directoryExists(String file) throws Exception { + return shellControl + .getShellDialect() + .directoryExists(shellControl, file) + .executeAndCheck(); + } + + @Override + public void directoryAccessible(String file) throws Exception { + shellControl.executeSimpleCommand(shellControl.getShellDialect().getCdCommand(file)); + } + + @Override + public Stream listFiles(String file) throws Exception { + return shellControl.getShellDialect().listFiles(this, shellControl, file); + } + + @Override + public List listRoots() throws Exception { + return shellControl.getShellDialect().listRoots(shellControl).toList(); + } + @Override public void close() { // In case the shell control is already in an invalid state, this operation might fail diff --git a/core/src/main/java/io/xpipe/core/store/DataFlow.java b/core/src/main/java/io/xpipe/core/store/DataFlow.java index f55dd79d..df7e8682 100644 --- a/core/src/main/java/io/xpipe/core/store/DataFlow.java +++ b/core/src/main/java/io/xpipe/core/store/DataFlow.java @@ -29,5 +29,4 @@ public enum DataFlow { public boolean hasOutput() { return this == OUTPUT || this == INPUT_OUTPUT; } - } 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 eccacb69..3fd37278 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 d6cda7d2..6a60bd94 100644 --- a/core/src/main/java/io/xpipe/core/store/DataStoreId.java +++ b/core/src/main/java/io/xpipe/core/store/DataStoreId.java @@ -43,9 +43,12 @@ 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"); + throw new IllegalArgumentException("Separator character " + SEPARATOR + " is not allowed in the names"); } if (Arrays.stream(names).anyMatch(s -> s.trim().length() == 0)) { @@ -67,9 +70,10 @@ 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/DataStoreState.java b/core/src/main/java/io/xpipe/core/store/DataStoreState.java index 4f2bfc1a..3a288f4b 100644 --- a/core/src/main/java/io/xpipe/core/store/DataStoreState.java +++ b/core/src/main/java/io/xpipe/core/store/DataStoreState.java @@ -15,17 +15,17 @@ public abstract class DataStoreState { public abstract void merge(DataStoreState newer); - @SneakyThrows - public String toString() { - var tree = JacksonMapper.getDefault().valueToTree(this); - return tree.toPrettyString(); - } - @SneakyThrows public DataStoreState deepCopy() { return JacksonMapper.getDefault().treeToValue(JacksonMapper.getDefault().valueToTree(this), getClass()); } + @Override + public final int hashCode() { + var tree = JacksonMapper.getDefault().valueToTree(this); + return tree.hashCode(); + } + @Override public final boolean equals(Object o) { if (this == o) { @@ -40,9 +40,9 @@ public abstract class DataStoreState { return tree.equals(otherTree); } - @Override - public final int hashCode() { + @SneakyThrows + public String toString() { var tree = JacksonMapper.getDefault().valueToTree(this); - return tree.hashCode(); + return tree.toPrettyString(); } } diff --git a/core/src/main/java/io/xpipe/core/store/ExpandedLifecycleStore.java b/core/src/main/java/io/xpipe/core/store/ExpandedLifecycleStore.java index d3cba23d..4ee263e9 100644 --- a/core/src/main/java/io/xpipe/core/store/ExpandedLifecycleStore.java +++ b/core/src/main/java/io/xpipe/core/store/ExpandedLifecycleStore.java @@ -1,6 +1,6 @@ package io.xpipe.core.store; -public interface ExpandedLifecycleStore extends DataStore{ +public interface ExpandedLifecycleStore extends DataStore { default void initializeValidate() throws Exception {} diff --git a/core/src/main/java/io/xpipe/core/store/FileNames.java b/core/src/main/java/io/xpipe/core/store/FileNames.java index 4e592aa9..48b3fb86 100644 --- a/core/src/main/java/io/xpipe/core/store/FileNames.java +++ b/core/src/main/java/io/xpipe/core/store/FileNames.java @@ -55,7 +55,7 @@ public class FileNames { return ""; } - return components.get(components.size() - 1); + return components.getLast(); } public static List splitHierarchy(String file) { 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 c445e8ad..f7e89216 100644 --- a/core/src/main/java/io/xpipe/core/store/FileSystem.java +++ b/core/src/main/java/io/xpipe/core/store/FileSystem.java @@ -17,84 +17,7 @@ import java.util.stream.Stream; public interface FileSystem extends Closeable, AutoCloseable { - @Value - @NonFinal - class FileEntry { - FileSystem fileSystem; - - @NonNull - @NonFinal - String path; - - @NonFinal - String extension; - - @NonFinal - String name; - - Instant date; - boolean hidden; - Boolean executable; - long size; - String mode; - - @NonNull - FileKind kind; - - public FileEntry( - FileSystem fileSystem, - @NonNull String path, - Instant date, - boolean hidden, - Boolean executable, - long size, - String mode, - @NonNull FileKind kind) { - this.fileSystem = fileSystem; - this.mode = mode; - this.kind = kind; - this.path = kind == FileKind.DIRECTORY ? FileNames.toDirectory(path) : path; - this.extension = FileNames.getExtension(path); - this.name = FileNames.getFileName(path); - this.date = date; - this.hidden = hidden; - this.executable = executable; - this.size = size; - } - - public void setPath(String path) { - this.path = path; - this.extension = FileNames.getExtension(path); - this.name = FileNames.getFileName(path); - } - - public FileEntry resolved() { - return this; - } - - public static FileEntry ofDirectory(FileSystem fileSystem, String path) { - return new FileEntry(fileSystem, path, Instant.now(), true, false, 0, null, FileKind.DIRECTORY); - } - } - - @Value - @EqualsAndHashCode(callSuper = true) - class LinkFileEntry extends FileEntry { - - @NonNull - FileEntry target; - - public LinkFileEntry( - @NonNull FileSystem fileSystem, @NonNull String path, Instant date, boolean hidden, Boolean executable, long size, String mode, @NonNull FileEntry target - ) { - super(fileSystem, path, date, hidden, executable, size, mode, FileKind.LINK); - this.target = target; - } - - public FileEntry resolved() { - return target; - } - } + long getFileSize(String file) throws Exception; FileSystemStore getStore(); @@ -104,7 +27,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; @@ -150,4 +73,88 @@ public interface FileSystem extends Closeable, AutoCloseable { } List listRoots() throws Exception; + + @Value + @NonFinal + class FileEntry { + FileSystem fileSystem; + Instant date; + boolean hidden; + Boolean executable; + long size; + String mode; + + @NonNull + FileKind kind; + + @NonNull + @NonFinal + String path; + + @NonFinal + String extension; + + @NonFinal + String name; + + public FileEntry( + FileSystem fileSystem, + @NonNull String path, + Instant date, + boolean hidden, + Boolean executable, + long size, + String mode, + @NonNull FileKind kind) { + this.fileSystem = fileSystem; + this.mode = mode; + this.kind = kind; + this.path = kind == FileKind.DIRECTORY ? FileNames.toDirectory(path) : path; + this.extension = FileNames.getExtension(path); + this.name = FileNames.getFileName(path); + this.date = date; + this.hidden = hidden; + this.executable = executable; + this.size = size; + } + + public static FileEntry ofDirectory(FileSystem fileSystem, String path) { + return new FileEntry(fileSystem, path, Instant.now(), true, false, 0, null, FileKind.DIRECTORY); + } + + public void setPath(String path) { + this.path = path; + this.extension = FileNames.getExtension(path); + this.name = FileNames.getFileName(path); + } + + public FileEntry resolved() { + return this; + } + } + + @Value + @EqualsAndHashCode(callSuper = true) + class LinkFileEntry extends FileEntry { + + @NonNull + FileEntry target; + + public LinkFileEntry( + @NonNull FileSystem fileSystem, + @NonNull String path, + Instant date, + boolean hidden, + Boolean executable, + long size, + String mode, + @NonNull FileEntry target) { + super(fileSystem, path, date, hidden, executable, size, mode, FileKind.LINK); + this.target = target; + } + + public FileEntry resolved() { + return target; + } + } } diff --git a/core/src/main/java/io/xpipe/core/store/InputStreamStore.java b/core/src/main/java/io/xpipe/core/store/InputStreamStore.java index 285fec44..8c968ef5 100644 --- a/core/src/main/java/io/xpipe/core/store/InputStreamStore.java +++ b/core/src/main/java/io/xpipe/core/store/InputStreamStore.java @@ -1,8 +1,5 @@ package io.xpipe.core.store; -import io.xpipe.core.store.DataFlow; -import io.xpipe.core.store.StreamDataStore; - import java.io.InputStream; /** @@ -16,11 +13,6 @@ public class InputStreamStore implements StreamDataStore { this.in = in; } - @Override - public InputStream openInput() { - return in; - } - @Override public DataFlow getFlow() { return DataFlow.INPUT; @@ -30,4 +22,9 @@ public class InputStreamStore implements StreamDataStore { public boolean canOpen() { return true; } + + @Override + public InputStream openInput() { + return in; + } } \ No newline at end of file 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 64bfee1d..0e9ac8d6 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/store/OutputStreamStore.java b/core/src/main/java/io/xpipe/core/store/OutputStreamStore.java index 6a028413..28d6a9d4 100644 --- a/core/src/main/java/io/xpipe/core/store/OutputStreamStore.java +++ b/core/src/main/java/io/xpipe/core/store/OutputStreamStore.java @@ -12,13 +12,18 @@ public class OutputStreamStore implements StreamDataStore { } @Override - public boolean isContentExclusivelyAccessible() { - return true; + public DataFlow getFlow() { + return DataFlow.OUTPUT; } @Override - public DataFlow getFlow() { - return DataFlow.OUTPUT; + public boolean canOpen() { + return false; + } + + @Override + public boolean isContentExclusivelyAccessible() { + return true; } @Override @@ -30,9 +35,4 @@ public class OutputStreamStore implements StreamDataStore { public OutputStream openOutput() { return out; } - - @Override - public boolean canOpen() { - return false; - } } diff --git a/core/src/main/java/io/xpipe/core/store/ShellStore.java b/core/src/main/java/io/xpipe/core/store/ShellStore.java index 97a25e4b..51cdc2ce 100644 --- a/core/src/main/java/io/xpipe/core/store/ShellStore.java +++ b/core/src/main/java/io/xpipe/core/store/ShellStore.java @@ -15,7 +15,7 @@ public interface ShellStore extends DataStore, LaunchableStore, FileSystemStore, } @Override - default ProcessControl prepareLaunchCommand() throws Exception { + default ProcessControl prepareLaunchCommand() { return control(); } diff --git a/core/src/main/java/io/xpipe/core/store/StatefulDataStore.java b/core/src/main/java/io/xpipe/core/store/StatefulDataStore.java index 0ce25656..0896c21e 100644 --- a/core/src/main/java/io/xpipe/core/store/StatefulDataStore.java +++ b/core/src/main/java/io/xpipe/core/store/StatefulDataStore.java @@ -20,23 +20,27 @@ public interface StatefulDataStore extends DataStore { @SuppressWarnings("unchecked") default T getState() { - return (T) DataStateProvider.get().getState(this, this::createDefaultState).deepCopy(); - } - - default T getState(Supplier def) { - return DataStateProvider.get().getState(this, def); + return (T) + DataStateProvider.get().getState(this, this::createDefaultState).deepCopy(); } default void setState(T val) { DataStateProvider.get().setState(this, val); } + default T getState(Supplier def) { + return DataStateProvider.get().getState(this, def); + } + @SneakyThrows @SuppressWarnings("unchecked") default Class getStateClass() { - var found = Arrays.stream(getClass().getDeclaredClasses()).filter(aClass -> DataStoreState.class.isAssignableFrom(aClass)).findAny(); + var found = Arrays.stream(getClass().getDeclaredClasses()) + .filter(aClass -> DataStoreState.class.isAssignableFrom(aClass)) + .findAny(); if (found.isEmpty()) { - throw new IllegalArgumentException("Store class " + getClass().getSimpleName() + " does not have a state class set"); + throw new IllegalArgumentException( + "Store class " + getClass().getSimpleName() + " does not have a state class set"); } return (Class) found.get(); diff --git a/core/src/main/java/io/xpipe/core/store/StdinDataStore.java b/core/src/main/java/io/xpipe/core/store/StdinDataStore.java index 5fea5920..4bbad954 100644 --- a/core/src/main/java/io/xpipe/core/store/StdinDataStore.java +++ b/core/src/main/java/io/xpipe/core/store/StdinDataStore.java @@ -1,7 +1,6 @@ package io.xpipe.core.store; import com.fasterxml.jackson.annotation.JsonTypeName; -import io.xpipe.core.store.StreamDataStore; import io.xpipe.core.util.JacksonizedValue; import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; diff --git a/core/src/main/java/io/xpipe/core/store/StdoutDataStore.java b/core/src/main/java/io/xpipe/core/store/StdoutDataStore.java index 211d4f39..c39f2920 100644 --- a/core/src/main/java/io/xpipe/core/store/StdoutDataStore.java +++ b/core/src/main/java/io/xpipe/core/store/StdoutDataStore.java @@ -1,7 +1,6 @@ package io.xpipe.core.store; import com.fasterxml.jackson.annotation.JsonTypeName; -import io.xpipe.core.store.StreamDataStore; import io.xpipe.core.util.JacksonizedValue; import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; diff --git a/core/src/main/java/io/xpipe/core/store/StreamDataStore.java b/core/src/main/java/io/xpipe/core/store/StreamDataStore.java index 50adb564..defd88d7 100644 --- a/core/src/main/java/io/xpipe/core/store/StreamDataStore.java +++ b/core/src/main/java/io/xpipe/core/store/StreamDataStore.java @@ -17,7 +17,7 @@ public interface StreamDataStore extends DataStore { * Checks whether this store can be opened. * This can be not the case for example if the underlying store does not exist. */ - default boolean canOpen() throws Exception { + default boolean canOpen() { return true; } @@ -35,7 +35,7 @@ public interface StreamDataStore extends DataStore { /** * Opens an input stream that can be used to read its data. */ - default InputStream openInput() throws Exception { + default InputStream openInput() { throw new UnsupportedOperationException("Can't open store input"); } @@ -54,7 +54,7 @@ public interface StreamDataStore extends DataStore { /** * Opens an output stream that can be used to write data. */ - default OutputStream openOutput() throws Exception { + default OutputStream openOutput() { throw new UnsupportedOperationException("Can't open store output"); } } 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 c5c38b28..db390afe 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 82f4d87b..fcb73e63 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,27 @@ 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/InPlaceSecretValue.java b/core/src/main/java/io/xpipe/core/util/InPlaceSecretValue.java new file mode 100644 index 00000000..b083d9e5 --- /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 InPlaceSecretValue(char[] secret) { + super(secret); + } + + public static InPlaceSecretValue of(String s) { + return new InPlaceSecretValue(s.toCharArray()); + } + + public static InPlaceSecretValue of(char[] c) { + return new InPlaceSecretValue(c); + } + + @Override + protected int getIterationCount() { + return 2048; + } + + 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}); + } + + @Override + public InPlaceSecretValue inPlace() { + return this; + } + + @Override + public String toString() { + return ""; + } +} diff --git a/core/src/main/java/io/xpipe/core/util/JacksonMapper.java b/core/src/main/java/io/xpipe/core/util/JacksonMapper.java index a6a72dc2..8699ffbd 100644 --- a/core/src/main/java/io/xpipe/core/util/JacksonMapper.java +++ b/core/src/main/java/io/xpipe/core/util/JacksonMapper.java @@ -17,15 +17,10 @@ public class JacksonMapper { private static final ObjectMapper BASE = new ObjectMapper(); private static final ObjectMapper INSTANCE; + @Getter private static boolean init = false; - public static T parse(String s, Class c) throws JsonProcessingException { - var mapper = getDefault(); - var tree = mapper.readTree(s); - return mapper.treeToValue(tree, c); - } - static { ObjectMapper objectMapper = BASE; objectMapper.enable(SerializationFeature.INDENT_OUTPUT); @@ -44,6 +39,12 @@ public class JacksonMapper { INSTANCE = BASE.copy(); } + public static T parse(String s, Class c) throws JsonProcessingException { + var mapper = getDefault(); + var tree = mapper.readTree(s); + return mapper.treeToValue(tree, c); + } + public static synchronized void configure(Consumer mapper) { mapper.accept(INSTANCE); } @@ -86,5 +87,4 @@ public class JacksonMapper { return INSTANCE; } - } diff --git a/core/src/main/java/io/xpipe/core/util/JacksonizedValue.java b/core/src/main/java/io/xpipe/core/util/JacksonizedValue.java index d0e77db5..8217420c 100644 --- a/core/src/main/java/io/xpipe/core/util/JacksonizedValue.java +++ b/core/src/main/java/io/xpipe/core/util/JacksonizedValue.java @@ -10,10 +10,10 @@ public class JacksonizedValue { public JacksonizedValue() {} - @SneakyThrows - public String toString() { + @Override + public final int hashCode() { var tree = JacksonMapper.getDefault().valueToTree(this); - return tree.toPrettyString(); + return tree.hashCode(); } @Override @@ -30,9 +30,9 @@ public class JacksonizedValue { return tree.equals(otherTree); } - @Override - public final int hashCode() { + @SneakyThrows + public String toString() { var tree = JacksonMapper.getDefault().valueToTree(this); - return tree.hashCode(); + return tree.toPrettyString(); } } diff --git a/core/src/main/java/io/xpipe/core/util/ProxyFunction.java b/core/src/main/java/io/xpipe/core/util/ProxyFunction.java index cb4e4f6d..015f0c94 100644 --- a/core/src/main/java/io/xpipe/core/util/ProxyFunction.java +++ b/core/src/main/java/io/xpipe/core/util/ProxyFunction.java @@ -25,13 +25,13 @@ public abstract class ProxyFunction { @SneakyThrows public ProxyFunction callAndCopy() { -// var proxyStore = ProxyProvider.get().getProxy(getProxyBase()); -// if (proxyStore != null) { -// return ProxyProvider.get().call(this, proxyStore); -// } else { -// callLocal(); -// return this; -// } + // var proxyStore = ProxyProvider.get().getProxy(getProxyBase()); + // if (proxyStore != null) { + // return ProxyProvider.get().call(this, proxyStore); + // } else { + // callLocal(); + // return this; + // } return null; } diff --git a/core/src/main/java/io/xpipe/core/util/ProxyManagerProvider.java b/core/src/main/java/io/xpipe/core/util/ProxyManagerProvider.java index 4b2768be..ec10a721 100644 --- a/core/src/main/java/io/xpipe/core/util/ProxyManagerProvider.java +++ b/core/src/main/java/io/xpipe/core/util/ProxyManagerProvider.java @@ -19,7 +19,7 @@ public abstract class ProxyManagerProvider { return INSTANCE; } - public abstract Optional checkCompatibility(ShellControl pc) throws Exception; + public abstract Optional checkCompatibility(ShellControl pc); - public abstract boolean setup(ShellControl pc) throws Exception; + public abstract boolean setup(ShellControl pc); } 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 00000000..f6ae9f56 --- /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 { + + 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 SecretReference ofUuid(UUID secretId) { + return new SecretReference(secretId, 0); + } +} 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 ea7d1516..3e8c4093 100644 --- a/core/src/main/java/io/xpipe/core/util/SecretValue.java +++ b/core/src/main/java/io/xpipe/core/util/SecretValue.java @@ -5,12 +5,11 @@ 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(); - static String toBase64e(byte[] b) { var base64 = Base64.getEncoder().encodeToString(b); return base64.replace("/", "-"); @@ -20,12 +19,28 @@ public interface SecretValue { return Base64.getDecoder().decode(s.replace("-", "/")); } + InPlaceSecretValue inPlace(); + default void withSecretValue(Consumer con) { var chars = getSecret(); con.accept(chars); 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/XPipeDaemonMode.java b/core/src/main/java/io/xpipe/core/util/XPipeDaemonMode.java index 5b7a3216..0085930b 100644 --- a/core/src/main/java/io/xpipe/core/util/XPipeDaemonMode.java +++ b/core/src/main/java/io/xpipe/core/util/XPipeDaemonMode.java @@ -19,6 +19,14 @@ public enum XPipeDaemonMode { @JsonProperty("gui") GUI("gui", List.of("gui", "desktop", "interface")); + private final String displayName; + private final List nameAlternatives; + + XPipeDaemonMode(String displayName, List nameAlternatives) { + this.displayName = displayName; + this.nameAlternatives = nameAlternatives; + } + public static XPipeDaemonMode get(String name) { return Arrays.stream(XPipeDaemonMode.values()) .filter(xPipeDaemonMode -> @@ -29,13 +37,4 @@ public enum XPipeDaemonMode { .map(XPipeDaemonMode::getDisplayName) .collect(Collectors.joining(", ")))); } - - private final String displayName; - - private final List nameAlternatives; - - XPipeDaemonMode(String displayName, List nameAlternatives) { - this.displayName = displayName; - this.nameAlternatives = nameAlternatives; - } } 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 7f351ba9..8aee357e 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 { @@ -43,12 +42,6 @@ public class XPipeInstallation { return Path.of(System.getProperty("user.home"), isStaging() ? ".xpipe-ptb" : ".xpipe"); } - public static String getDataDir(ShellControl p) throws Exception { - var name = isStaging() ? ".xpipe-ptb" : ".xpipe"; - var dir = p.getOsType().getHomeDirectory(p); - return FileNames.join(dir, name); - } - private static String getPkgId() { return isStaging() ? "io.xpipe.xpipe-ptb" : "io.xpipe.xpipe"; } @@ -58,8 +51,7 @@ public class XPipeInstallation { var suffix = (arguments != null ? " " + arguments : ""); var modeOption = mode != null ? " --mode " + mode.getDisplayName() : ""; if (OsType.getLocal().equals(OsType.LINUX)) { - return "nohup \"" + installationBase + "/app/bin/xpiped\"" + modeOption + suffix - + " & disown"; + return "nohup \"" + installationBase + "/app/bin/xpiped\"" + modeOption + suffix + " & disown"; } else if (OsType.getLocal().equals(OsType.MACOS)) { return "open \"" + installationBase + "\" --args" + modeOption + suffix; } @@ -78,7 +70,8 @@ public class XPipeInstallation { public static Path getCurrentInstallationBasePath() { // We should always have a command associated with the current process, otherwise something went seriously wrong // Resolve any possible links to a real path - Path path = toRealPathIfPossible(Path.of(ProcessHandle.current().info().command().orElseThrow())); + Path path = toRealPathIfPossible( + Path.of(ProcessHandle.current().info().command().orElseThrow())); // Check if the process was started using a relative path, and adapt it if necessary if (!path.isAbsolute()) { path = toRealPathIfPossible(Path.of(System.getProperty("user.dir")).resolve(path)); @@ -87,7 +80,8 @@ public class XPipeInstallation { var name = path.getFileName().toString(); // Check if we launched the JVM via a start script instead of the native executable if (name.endsWith("java") || name.endsWith("java.exe")) { - // If we are not an image, we are probably running in a development environment where we want to use the working directory + // If we are not an image, we are probably running in a development environment where we want to use the + // working directory var isImage = ModuleHelper.isImage(); if (!isImage) { return Path.of(System.getProperty("user.dir")); @@ -212,16 +206,35 @@ 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 +311,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 +321,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 +331,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 +341,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/test/java/io/xpipe/core/test/DataStoreIdTest.java b/core/src/test/java/io/xpipe/core/test/DataStoreIdTest.java index 5381915c..9a1f963c 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/dist/base.gradle b/dist/base.gradle index f2a24549..1530d6ea 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" @@ -178,8 +190,8 @@ if (org.gradle.internal.os.OperatingSystem.current().isWindows()) { } file("$distDir/$app/Contents/Resources/scripts/uninstall.sh").text = file("$distDir/$app/Contents/Resources/scripts/uninstall.sh").text .replaceAll("__PRODUCT__", productName) - .replaceAll("__PACKAGE_ID__", "io.xpipe.$kebapProductName") - .replaceAll("__VERSION__", version.toString()) + .replaceAll("__PRODUCT_KEBAP__", kebapProductName) + .replaceAll("__VERSION__", versionString) def debugArguments = file("$projectDir/debug/debug_arguments.txt").text.lines().map(s -> '"' + s + '"').collect(Collectors.joining( ' ')) diff --git a/dist/build.gradle b/dist/build.gradle index d22e0091..615ef758 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-6.md b/dist/changelogs/8.0-6.md new file mode 100644 index 00000000..da1747a6 --- /dev/null +++ b/dist/changelogs/8.0-6.md @@ -0,0 +1,18 @@ +## Temporary containers + +You can now run a temporary docker container using a specified image that will get automatically removed once it is stopped. The container will keep running even if the image does not have any command specified that will run. + +This can be useful if you quickly want to set up a certain environment by using a certain container image, e.g. a simple `ubuntu` image. You can then enter the container as normal in XPipe, perform your operations, and stop the container once it's no longer needed. It is then removed automatically. + +## macOS tray and dock handling + +Due to some confusion, XPipe will no longer use the system tray in macOS as an option when minimizing. It will instead conform to the usual macOS app handling that allows to reopen the window by clicking on the dock icon. + +## Other changes + +- Add option to skip connection validation +- Introduce new changelog implementation that will be able to display the changelog relevant when upgrading from you current version, including all intermediate versions +- Auto expand connections display when a new child is added +- Fix opnsense and PFsense systems not working +- Fix elevation not working in some cases and throwing errors +- Fix debug mode not working \ No newline at end of file diff --git a/dist/changelogs/8.0.md b/dist/changelogs/8.0.md new file mode 100644 index 00000000..81c7697d --- /dev/null +++ b/dist/changelogs/8.0.md @@ -0,0 +1,102 @@ +This update includes many changes that 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! + +Note that on Windows the automatic updater still has a few issues with race conditions if you are upgrading from 1.7.16. If the automatic update fails, you can still install 8.0 manually by downloading and installing it from https://github.com/xpipe-io/xpipe/releases. + +## 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 (Professional feature) + +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 start one with XPipe. + +This feature will be available in the professional version, but is also available in the free professional edition preview after release. + +## Improved professional edition preview + +Any new professional features, such as the Proxmox support, will be available for free for a couple of weeks after their initial release. There is now a new dialog available to allow you to quickly set up the XPipe preview plan. + +This allows anyone interested in playing around with new features to do so without limitation and no commitment. + +## Git For Windows shell environments + +The git installation on Windows comes with its own posix environment, which some people use to make use of standard Linux functionalities on Windows if they have not moved to WSL yet. This update brings full support to add this shell environment as well via the automatic detection functionality. + +## File browser improvements + +The file browser has been reworked in terms of performance and reliability. Any errors that can occur 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. + +There are also a couple more changes included: +- 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 +- The transfer pane now also allows file drops from outside the window to make it more intuitive + +## Kubernetes configs and namespaces + +This update adds support to also add connections from other kubeconfig files. + +Furthermore, you can also choose to use any namespace you want. This is useful in cases where you have not set up a context for every namespace you have. + +The Kubernetes support is also now available in the pro preview after release. + +## 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. + +There has been a big focus on providing finer-grained control over security settings, which can be especially useful in enterprise contexts. + +## 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). + +The implementation also now supports dumb shells that can be reached via ssh for example, but support still has to be implemented manually. Since I currently don't own any typical hardware like various routers and links, e.g. from cisco, I did not have the ability yet to do this. So if you own any hardware you would like to see supported, open an issue and share the typical output that is printed upon connection, and it can easily be supported. + +For now, it should work with MikroTik routers at least. + +## Temporary containers + +You can now run a temporary docker container using a specified image that will get automatically removed once it is stopped. The container will keep running even if the image does not have any command specified that will run. + +This can be useful if you quickly want to set up a certain environment by using a certain container image, e.g. a simple `ubuntu` image. You can then enter the container as normal in XPipe, perform your operations, and stop the container once it's no longer needed. It is then removed automatically. + +## macOS tray and dock handling + +Due to some confusion, XPipe will no longer use the system tray in macOS as an option when minimizing. It will instead conform to the usual macOS app handling that allows to reopen the window by clicking on the dock icon. + +## 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. + +## Timeout handling + +The timeout model has been reworked. It is now set to a fixed amount of 30s while any active password prompts do no longer count towards it, meaning you can take your time when typing your passwords. An increased timeout value also allows for better handling of third party authentication schemes that XPipe has no control over, e.g. ones that will open a site in your browser for authentication. + +## Other changes + +- Add option to skip connection validation +- Add ability to easily add files to the git vault data directory +- Introduce new changelog implementation that will be able to display the changelog relevant when upgrading from you current version, including all intermediate versions +- Auto expand connections display when a new child is added +- Fix elevation not working in some cases and throwing errors +- Improve git vault performance +- Fix macOS updater and installation script sometimes only uninstalling existing version without installing new one +- Fix scaling issues on Linux by providing a separate scaling option +- Fix possible encoding issues on Windows with passwords that contained non-ASCII characters +- Support opening ssh: URLs without username as well +- Fix Linux OS logo sometimes showing wrongly or not at all diff --git a/dist/jpackage.gradle b/dist/jpackage.gradle index c4df627e..1b657a0c 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/atlantafx.properties b/dist/licenses/atlantafx.properties index 054478eb..e39a2acd 100644 --- a/dist/licenses/atlantafx.properties +++ b/dist/licenses/atlantafx.properties @@ -1,4 +1,4 @@ name=AtlantaFX -version=2.0.0 +version=2.0.1 license=MIT License link=https://github.com/mkpaz/atlantafx \ No newline at end of file diff --git a/dist/licenses/commons-io.properties b/dist/licenses/commons-io.properties index 15ef19ad..7e399cf4 100644 --- a/dist/licenses/commons-io.properties +++ b/dist/licenses/commons-io.properties @@ -1,4 +1,4 @@ name=Commons IO -version=2.8.0 +version=2.15.1 license=Apache License 2.0 link=https://commons.apache.org/proper/commons-io/ \ No newline at end of file diff --git a/dist/licenses/github-api.properties b/dist/licenses/github-api.properties index d4b50b41..b7fe0601 100644 --- a/dist/licenses/github-api.properties +++ b/dist/licenses/github-api.properties @@ -1,4 +1,4 @@ name=Java API for GitHub -version=1.301 +version=1.318 license=MIT License link=https://github.com/hub4j/github-api \ No newline at end of file diff --git a/dist/licenses/graalvm.properties b/dist/licenses/graalvm.properties index 29b49333..0f02199e 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/jackson.properties b/dist/licenses/jackson.properties index ddf93acb..823f2f1c 100644 --- a/dist/licenses/jackson.properties +++ b/dist/licenses/jackson.properties @@ -1,4 +1,4 @@ name=Jackson Databind -version=2.15.2 +version=2.16.1 license=Apache License 2.0 link=https://github.com/FasterXML/jackson-databind \ No newline at end of file diff --git a/dist/licenses/jna.properties b/dist/licenses/jna.properties index 91e8431c..5b906680 100644 --- a/dist/licenses/jna.properties +++ b/dist/licenses/jna.properties @@ -1,4 +1,4 @@ name=jna -version=5.12.1 +version=5.14.0 license=Apache License 2.0 link=https://github.com/java-native-access/jna/ \ No newline at end of file diff --git a/dist/licenses/lombok.properties b/dist/licenses/lombok.properties index 38cc3128..0ec10fa7 100644 --- a/dist/licenses/lombok.properties +++ b/dist/licenses/lombok.properties @@ -1,4 +1,4 @@ name=lombok -version=1.18.16 +version=1.18.30 license=MIT License link=https://projectlombok.org/ \ No newline at end of file diff --git a/dist/licenses/openjfx.properties b/dist/licenses/openjfx.properties index e915f84b..ed137f4c 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/picocli.properties b/dist/licenses/picocli.properties index ac687ca9..e2447f5f 100644 --- a/dist/licenses/picocli.properties +++ b/dist/licenses/picocli.properties @@ -1,4 +1,4 @@ name=picocli -version=4.6.3 +version=4.7.5 license=Apache 2.0 License link=https://picocli.info/ \ No newline at end of file diff --git a/dist/licenses/sentry.properties b/dist/licenses/sentry.properties index a7ceb3ac..7755d64a 100644 --- a/dist/licenses/sentry.properties +++ b/dist/licenses/sentry.properties @@ -1,4 +1,4 @@ name=Sentry Java -version=5.6.0 +version=7.3.0 license=MIT License link=https://github.com/getsentry/sentry-java \ No newline at end of file diff --git a/dist/licenses/slf4j.properties b/dist/licenses/slf4j.properties index 03747885..6f696bed 100644 --- a/dist/licenses/slf4j.properties +++ b/dist/licenses/slf4j.properties @@ -1,4 +1,4 @@ name=SLF4J -version=2.0.0-alpha2 +version=2.0.11 license=MIT License link=https://www.slf4j.org/ \ No newline at end of file diff --git a/dist/licenses/validatorfx.properties b/dist/licenses/validatorfx.properties index 8443be1b..c5a93d14 100644 --- a/dist/licenses/validatorfx.properties +++ b/dist/licenses/validatorfx.properties @@ -1,4 +1,4 @@ name=ValidatorFX -version=0.3.1 +version=0.4.2 license=BSD 3-Clause link=https://github.com/effad/ValidatorFX \ 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 00000000..4b0db548 --- /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 00000000..1ec7eb58 --- /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 eca31e1d..c34d808e 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/localTest/java/module-info.java b/ext/base/src/localTest/java/module-info.java new file mode 100644 index 00000000..f5e0d8e6 --- /dev/null +++ b/ext/base/src/localTest/java/module-info.java @@ -0,0 +1,10 @@ +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 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 00000000..2e75f5fb --- /dev/null +++ b/ext/base/src/localTest/java/test/Test.java @@ -0,0 +1,13 @@ +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/SelfReferentialStore.java b/ext/base/src/main/java/io/xpipe/ext/base/SelfReferentialStore.java index 3c538609..9baa8936 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/SelfReferentialStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/SelfReferentialStore.java @@ -9,10 +9,14 @@ import java.util.UUID; public interface SelfReferentialStore extends DataStore { default DataStoreEntry getSelfEntry() { - return DataStorage.get().getStoreEntryIfPresent(this).or(() -> { - return DataStorage.get().getStoreEntryInProgressIfPresent(this); - }).orElseGet(() -> { - return DataStoreEntry.createNew(UUID.randomUUID(),DataStorage.DEFAULT_CATEGORY_UUID, "Invalid", this); - }); + return DataStorage.get() + .getStoreEntryIfPresent(this) + .or(() -> { + return DataStorage.get().getStoreEntryInProgressIfPresent(this); + }) + .orElseGet(() -> { + return DataStoreEntry.createNew( + UUID.randomUUID(), DataStorage.DEFAULT_CATEGORY_UUID, "Invalid", this); + }); } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/BrowseStoreAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/BrowseStoreAction.java index 51c52b08..aec80275 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/BrowseStoreAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/BrowseStoreAction.java @@ -13,27 +13,20 @@ import lombok.Value; public class BrowseStoreAction implements ActionProvider { - @Value - static class Action implements ActionProvider.Action { - - DataStoreEntry entry; - - @Override - public boolean requiresJavaFXPlatform() { - return true; - } - - @Override - public void execute() { - BrowserModel.DEFAULT.openFileSystemAsync(entry.ref(),null, new SimpleBooleanProperty()); - AppLayoutModel.get().selectBrowser(); - } - } - @Override public DataStoreCallSite getDataStoreCallSite() { return new DataStoreCallSite() { + @Override + public ActionProvider.Action createAction(DataStoreEntryRef store) { + return new Action(store.get()); + } + + @Override + public Class getApplicableClass() { + return ShellStore.class; + } + @Override public boolean isMajor(DataStoreEntryRef o) { return true; @@ -48,16 +41,23 @@ public class BrowseStoreAction implements ActionProvider { public String getIcon(DataStoreEntryRef store) { return "mdi2f-folder-open-outline"; } - - @Override - public ActionProvider.Action createAction(DataStoreEntryRef store) { - return new Action(store.get()); - } - - @Override - public Class getApplicableClass() { - return ShellStore.class; - } }; } + + @Value + static class Action implements ActionProvider.Action { + + DataStoreEntry entry; + + @Override + public boolean requiresJavaFXPlatform() { + return true; + } + + @Override + public void execute() { + BrowserModel.DEFAULT.openFileSystemAsync(entry.ref(), null, new SimpleBooleanProperty()); + AppLayoutModel.get().selectBrowser(); + } + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/CloneStoreAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/CloneStoreAction.java index 7eda77fb..509761bb 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/CloneStoreAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/CloneStoreAction.java @@ -11,23 +11,6 @@ import lombok.Value; public class CloneStoreAction implements ActionProvider { - @Value - static class Action implements ActionProvider.Action { - - DataStoreEntry store; - - @Override - public boolean requiresJavaFXPlatform() { - return false; - } - - @Override - public void execute() { - DataStorage.get().addStoreEntryIfNotPresent( - DataStoreEntry.createNew(store.getName() + " (Copy)",store.getStore())); - } - } - @Override public DataStoreCallSite getDataStoreCallSite() { return new DataStoreCallSite<>() { @@ -63,4 +46,21 @@ public class CloneStoreAction implements ActionProvider { } }; } + + @Value + static class Action implements ActionProvider.Action { + + DataStoreEntry store; + + @Override + public boolean requiresJavaFXPlatform() { + return false; + } + + @Override + public void execute() { + DataStorage.get() + .addStoreEntryIfNotPresent(DataStoreEntry.createNew(store.getName() + " (Copy)", store.getStore())); + } + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/DeleteStoreChildrenAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/DeleteStoreChildrenAction.java index ad9fd1cd..f57fbf48 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/DeleteStoreChildrenAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/DeleteStoreChildrenAction.java @@ -5,29 +5,13 @@ 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.core.store.DataStore; import io.xpipe.app.util.FixedHierarchyStore; +import io.xpipe.core.store.DataStore; import javafx.beans.value.ObservableValue; import lombok.Value; public class DeleteStoreChildrenAction implements ActionProvider { - @Value - static class Action implements ActionProvider.Action { - - DataStoreEntry store; - - @Override - public boolean requiresJavaFXPlatform() { - return false; - } - - @Override - public void execute() { - DataStorage.get().deleteChildren(store); - } - } - @Override public DataStoreCallSite getDataStoreCallSite() { return new DataStoreCallSite<>() { @@ -49,10 +33,8 @@ public class DeleteStoreChildrenAction implements ActionProvider { @Override public boolean isApplicable(DataStoreEntryRef o) { - return !(o.getStore() instanceof FixedHierarchyStore) && DataStorage.get() - .getStoreChildren(o.get()) - .size() - > 1; + return !(o.getStore() instanceof FixedHierarchyStore) + && DataStorage.get().getStoreChildren(o.get()).size() > 1; } @Override @@ -66,4 +48,20 @@ public class DeleteStoreChildrenAction implements ActionProvider { } }; } + + @Value + static class Action implements ActionProvider.Action { + + DataStoreEntry store; + + @Override + public boolean requiresJavaFXPlatform() { + return false; + } + + @Override + public void execute() { + DataStorage.get().deleteChildren(store); + } + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/EditStoreAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/EditStoreAction.java index 516a548c..0e0aa725 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/EditStoreAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/EditStoreAction.java @@ -11,64 +11,10 @@ import lombok.Value; public class EditStoreAction implements ActionProvider { - @Value - static class Action implements ActionProvider.Action { - - DataStoreEntry store; - - @Override - public boolean requiresJavaFXPlatform() { - return true; - } - - @Override - public void execute() { - StoreCreationComp.showEdit(store); - } - } - - @Override - public DefaultDataStoreCallSite getDefaultDataStoreCallSite() { - return new DefaultDataStoreCallSite<>() { - @Override - public boolean isApplicable(DataStoreEntryRef o) { - if (o.get() - .getValidity() == DataStoreEntry.Validity.LOAD_FAILED) { - return false; - } - - return o.get() - .getValidity() - .equals(DataStoreEntry.Validity.INCOMPLETE) || o.get().getProvider().editByDefault(); - } - - @Override - public ActionProvider.Action createAction(DataStoreEntryRef store) { - return new Action(store.get()); - } - - @Override - public Class getApplicableClass() { - return DataStore.class; - } - }; - } - @Override public DataStoreCallSite getDataStoreCallSite() { return new DataStoreCallSite<>() { - @Override - public boolean isMajor(DataStoreEntryRef o) { - var provider = o.get().getProvider(); - return provider.shouldEdit(); - } - - @Override - public ActiveType activeType() { - return ActiveType.ALWAYS_ENABLE; - } - @Override public boolean isSystemAction() { return true; @@ -84,6 +30,12 @@ public class EditStoreAction implements ActionProvider { return DataStore.class; } + @Override + public boolean isMajor(DataStoreEntryRef o) { + var provider = o.get().getProvider(); + return provider.shouldEdit(); + } + @Override public ObservableValue getName(DataStoreEntryRef store) { return AppI18n.observable("base.edit"); @@ -93,6 +45,52 @@ public class EditStoreAction implements ActionProvider { public String getIcon(DataStoreEntryRef store) { return "mdal-edit"; } + + @Override + public ActiveType activeType() { + return ActiveType.ALWAYS_ENABLE; + } }; } + + @Override + public DefaultDataStoreCallSite getDefaultDataStoreCallSite() { + return new DefaultDataStoreCallSite<>() { + @Override + public ActionProvider.Action createAction(DataStoreEntryRef store) { + return new Action(store.get()); + } + + @Override + public Class getApplicableClass() { + return DataStore.class; + } + + @Override + public boolean isApplicable(DataStoreEntryRef o) { + if (o.get().getValidity() == DataStoreEntry.Validity.LOAD_FAILED) { + return false; + } + + return o.get().getValidity().equals(DataStoreEntry.Validity.INCOMPLETE) + || o.get().getProvider().editByDefault(); + } + }; + } + + @Value + static class Action implements ActionProvider.Action { + + DataStoreEntry store; + + @Override + public boolean requiresJavaFXPlatform() { + return true; + } + + @Override + public void execute() { + StoreCreationComp.showEdit(store); + } + } } 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 3c70f2f3..874e2f9c 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; @@ -14,33 +14,9 @@ import lombok.Value; public class LaunchAction implements ActionProvider { - @Value - static class Action implements ActionProvider.Action { - - DataStoreEntry entry; - - @Override - public boolean requiresJavaFXPlatform() { - return false; - } - - @Override - 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())); - return; - } - - if (entry.getStore() instanceof LaunchableStore s) { - var command = s.prepareLaunchCommand(); - if (command == null) { - return; - } - - TerminalHelper.open(entry, storeName, command); - } - } + @Override + public String getId() { + return "launch"; } @Override @@ -52,13 +28,6 @@ public class LaunchAction implements ActionProvider { return true; } - @Override - public boolean isApplicable(DataStoreEntryRef o) { - return o.get() - .getValidity() - .isUsable() && o.getStore().canLaunch(); - } - @Override public ActionProvider.Action createAction(DataStoreEntryRef store) { return new Action(store.get()); @@ -69,6 +38,11 @@ public class LaunchAction implements ActionProvider { return LaunchableStore.class; } + @Override + public boolean isApplicable(DataStoreEntryRef o) { + return o.get().getValidity().isUsable() && o.getStore().canLaunch(); + } + @Override public ObservableValue getName(DataStoreEntryRef store) { return AppI18n.observable("launch"); @@ -81,22 +55,10 @@ public class LaunchAction implements ActionProvider { }; } - @Override - public String getId() { - return "launch"; - } - @Override public DefaultDataStoreCallSite getDefaultDataStoreCallSite() { return new DefaultDataStoreCallSite() { - @Override - public boolean isApplicable(DataStoreEntryRef o) { - return o.get() - .getValidity() - .isUsable() && o.getStore().canLaunch(); - } - @Override public ActionProvider.Action createAction(DataStoreEntryRef store) { return new Action(store.get()); @@ -106,6 +68,40 @@ public class LaunchAction implements ActionProvider { public Class getApplicableClass() { return LaunchableStore.class; } + + @Override + public boolean isApplicable(DataStoreEntryRef o) { + return o.get().getValidity().isUsable() && o.getStore().canLaunch(); + } }; } + + @Value + static class Action implements ActionProvider.Action { + + DataStoreEntry entry; + + @Override + public boolean requiresJavaFXPlatform() { + return false; + } + + @Override + public void execute() throws Exception { + var storeName = DataStorage.get().getStoreDisplayName(entry); + if (entry.getStore() instanceof ShellStore s) { + TerminalLauncher.open(entry, storeName, null, ScriptStore.controlWithDefaultScripts(s.control())); + return; + } + + if (entry.getStore() instanceof LaunchableStore s) { + var command = s.prepareLaunchCommand(); + if (command == null) { + return; + } + + TerminalLauncher.open(entry, storeName, null, command); + } + } + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchShortcutAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchShortcutAction.java index b3ee56a1..a0a7a27b 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchShortcutAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchShortcutAction.java @@ -11,22 +11,6 @@ import lombok.Value; public class LaunchShortcutAction implements ActionProvider { - @Value - static class Action implements ActionProvider.Action { - - DataStoreEntry entry; - - @Override - public boolean requiresJavaFXPlatform() { - return false; - } - - @Override - public void execute() throws Exception { - DesktopShortcuts.create("xpipe://launch/" + entry.getUuid().toString(), entry.getName()); - } - } - @Override public DataStoreCallSite getDataStoreCallSite() { return new DataStoreCallSite() { @@ -50,7 +34,22 @@ public class LaunchShortcutAction implements ActionProvider { public String getIcon(DataStoreEntryRef store) { return "mdi2c-code-greater-than"; } - }; } + + @Value + static class Action implements ActionProvider.Action { + + DataStoreEntry entry; + + @Override + public boolean requiresJavaFXPlatform() { + return false; + } + + @Override + public void execute() throws Exception { + DesktopShortcuts.create("xpipe://launch/" + entry.getUuid().toString(), entry.getName()); + } + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/ObserveStoreAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/ObserveStoreAction.java index fa76a7e0..85212dfa 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/ObserveStoreAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/ObserveStoreAction.java @@ -9,22 +9,6 @@ import lombok.Value; public class ObserveStoreAction implements ActionProvider { - @Value - static class Action implements ActionProvider.Action { - - DataStoreEntryRef store; - - @Override - public boolean requiresJavaFXPlatform() { - return true; - } - - @Override - public void execute() { - store.getStore().toggleObserverState(!store.getStore().getObserverState()); - } - } - @Override public DataStoreCallSite getDataStoreCallSite() { return new DataStoreCallSite() { @@ -41,7 +25,9 @@ public class ObserveStoreAction implements ActionProvider { @Override public ObservableValue getName(DataStoreEntryRef store) { - return store.getStore().getObserverState() ? AppI18n.observable("base.stopObserve") : AppI18n.observable("base.observe"); + return store.getStore().getObserverState() + ? AppI18n.observable("base.stopObserve") + : AppI18n.observable("base.observe"); } @Override @@ -50,4 +36,20 @@ public class ObserveStoreAction implements ActionProvider { } }; } + + @Value + static class Action implements ActionProvider.Action { + + DataStoreEntryRef store; + + @Override + public boolean requiresJavaFXPlatform() { + return true; + } + + @Override + public void execute() { + store.getStore().toggleObserverState(!store.getStore().getObserverState()); + } + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/RefreshStoreAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/RefreshStoreAction.java index 47711ec9..7da0c066 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/RefreshStoreAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/RefreshStoreAction.java @@ -9,54 +9,12 @@ import io.xpipe.app.util.FixedHierarchyStore; import javafx.beans.value.ObservableValue; import lombok.Value; -public class RefreshStoreAction implements ActionProvider { - - @Value - static class Action implements ActionProvider.Action { - - DataStoreEntry store; - - @Override - public boolean requiresJavaFXPlatform() { - return false; - } - - @Override - public void execute() throws Exception { - DataStorage.get().refreshChildren(store); - } - } - - @Override - public DefaultDataStoreCallSite getDefaultDataStoreCallSite() { - return new DefaultDataStoreCallSite<>() { - - @Override - public boolean isApplicable(DataStoreEntryRef o) { - return DataStorage.get().getStoreChildren(o.get()).size() == 0; - } - - @Override - public ActionProvider.Action createAction(DataStoreEntryRef store) { - return new Action(store.get()); - } - - @Override - public Class getApplicableClass() { - return FixedHierarchyStore.class; - } - }; - } +public class RefreshStoreAction implements ActionProvider { @Override public ActionProvider.DataStoreCallSite getDataStoreCallSite() { return new ActionProvider.DataStoreCallSite() { - @Override - public boolean isMajor(DataStoreEntryRef o) { - return true; - } - @Override public ActionProvider.Action createAction(DataStoreEntryRef store) { return new Action(store.get()); @@ -67,6 +25,11 @@ public class RefreshStoreAction implements ActionProvider { return FixedHierarchyStore.class; } + @Override + public boolean isMajor(DataStoreEntryRef o) { + return true; + } + @Override public ObservableValue getName(DataStoreEntryRef store) { return AppI18n.observable("base.refresh"); @@ -78,4 +41,41 @@ public class RefreshStoreAction implements ActionProvider { } }; } + + @Override + public DefaultDataStoreCallSite getDefaultDataStoreCallSite() { + return new DefaultDataStoreCallSite<>() { + + @Override + public ActionProvider.Action createAction(DataStoreEntryRef store) { + return new Action(store.get()); + } + + @Override + public Class getApplicableClass() { + return FixedHierarchyStore.class; + } + + @Override + public boolean isApplicable(DataStoreEntryRef o) { + return DataStorage.get().getStoreChildren(o.get()).size() == 0; + } + }; + } + + @Value + static class Action implements ActionProvider.Action { + + DataStoreEntry store; + + @Override + public boolean requiresJavaFXPlatform() { + return false; + } + + @Override + public void execute() { + DataStorage.get().refreshChildren(store); + } + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/SampleAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/SampleAction.java index 75c27d98..9880669f 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/SampleAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/SampleAction.java @@ -17,6 +17,44 @@ import java.io.InputStreamReader; public class SampleAction implements ActionProvider { + @Override + public DataStoreCallSite getDataStoreCallSite() { + // Call sites represent different ways of invoking the action. + // In this case, this represents a button that is shown for all stored shell connections. + return new DataStoreCallSite() { + + @Override + public Action createAction(DataStoreEntryRef store) { + return new Action(store.get()); + } + + @Override + public Class getApplicableClass() { + // For which general type of connection store to make this action available. + return ShellStore.class; + } + + @Override + public boolean isApplicable(DataStoreEntryRef o) { + // Allows you to individually check whether this action should be available for the specific store. + // In this case it should only be available for remote shell connections, not local ones. + return !ShellStore.isLocal(o.getStore()); + } + + @Override + public ObservableValue getName(DataStoreEntryRef store) { + // The displayed name of the action, allows you to use translation keys. + return AppI18n.observable("installConnector"); + } + + @Override + public String getIcon(DataStoreEntryRef store) { + // The ikonli icon of the button. + return "mdi2c-code-greater-than"; + } + }; + } + @Value static class Action implements ActionProvider.Action { @@ -105,42 +143,4 @@ public class SampleAction implements ActionProvider { } } } - - @Override - public DataStoreCallSite getDataStoreCallSite() { - // Call sites represent different ways of invoking the action. - // In this case, this represents a button that is shown for all stored shell connections. - return new DataStoreCallSite() { - - @Override - public Action createAction(DataStoreEntryRef store) { - return new Action(store.get()); - } - - @Override - public Class getApplicableClass() { - // For which general type of connection store to make this action available. - return ShellStore.class; - } - - @Override - public boolean isApplicable(DataStoreEntryRef o) { - // Allows you to individually check whether this action should be available for the specific store. - // In this case it should only be available for remote shell connections, not local ones. - return !ShellStore.isLocal(o.getStore()); - } - - @Override - public ObservableValue getName(DataStoreEntryRef store) { - // The displayed name of the action, allows you to use translation keys. - return AppI18n.observable("installConnector"); - } - - @Override - public String getIcon(DataStoreEntryRef store) { - // The ikonli icon of the button. - return "mdi2c-code-greater-than"; - } - }; - } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/ScanAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/ScanAction.java index c12d4649..9ba46c34 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/ScanAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/ScanAction.java @@ -11,26 +11,20 @@ import lombok.Value; public class ScanAction implements ActionProvider { - @Value - static class Action implements ActionProvider.Action { - - DataStoreEntry entry; - - @Override - public boolean requiresJavaFXPlatform() { - return true; - } - - @Override - public void execute() { - ScanAlert.showAsync(entry); - } - } - @Override public DataStoreCallSite getDataStoreCallSite() { return new DataStoreCallSite() { + @Override + public ActionProvider.Action createAction(DataStoreEntryRef store) { + return new Action(store.get()); + } + + @Override + public Class getApplicableClass() { + return ShellStore.class; + } + @Override public boolean isMajor(DataStoreEntryRef o) { return o.get().getProvider().shouldHaveChildren(); @@ -50,16 +44,22 @@ public class ScanAction implements ActionProvider { public String getIcon(DataStoreEntryRef store) { return "mdi2m-magnify-scan"; } - - @Override - public ActionProvider.Action createAction(DataStoreEntryRef store) { - return new Action(store.get()); - } - - @Override - public Class getApplicableClass() { - return ShellStore.class; - } }; } + + @Value + static class Action implements ActionProvider.Action { + + DataStoreEntry entry; + + @Override + public boolean requiresJavaFXPlatform() { + return true; + } + + @Override + public void execute() { + ScanAlert.showAsync(entry); + } + } } 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 95270f24..61420e41 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,36 +6,13 @@ 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; public class ShareStoreAction implements ActionProvider { - @Value - static class Action implements ActionProvider.Action { - - DataStoreEntry store; - - @Override - public boolean requiresJavaFXPlatform() { - return false; - } - - public static String create(DataStore store) { - return "xpipe://addStore/" - + SecretHelper.encryptInPlace(store.toString()).getEncryptedValue(); - } - - @Override - public void execute() { - var url = create(store.getStore()); - AppActionLinkDetector.setLastDetectedAction(url); - ClipboardHelper.copyUrl(url); - } - } - @Override public DataStoreCallSite getDataStoreCallSite() { return new DataStoreCallSite<>() { @@ -66,4 +43,26 @@ public class ShareStoreAction implements ActionProvider { } }; } + + @Value + static class Action implements ActionProvider.Action { + + DataStoreEntry store; + + public static String create(DataStore store) { + return "xpipe://addStore/" + InPlaceSecretValue.of(store.toString()).getEncryptedValue(); + } + + @Override + public boolean requiresJavaFXPlatform() { + return false; + } + + @Override + public void execute() { + var url = create(store.getStore()); + AppActionLinkDetector.setLastDetectedAction(url); + ClipboardHelper.copyUrl(url); + } + } } 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 29eaf460..c32c2eb9 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 @@ -1,14 +1,14 @@ package io.xpipe.ext.base.action; -import io.xpipe.app.comp.store.StoreViewState; import io.xpipe.app.comp.store.StoreCreationComp; +import io.xpipe.app.comp.store.StoreViewState; 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.core.store.LaunchableStore; -import io.xpipe.core.util.DefaultSecretValue; +import io.xpipe.app.util.TerminalLauncher; import io.xpipe.core.store.DataStore; +import io.xpipe.core.store.LaunchableStore; +import io.xpipe.core.util.InPlaceSecretValue; import io.xpipe.core.util.JacksonMapper; import lombok.Value; @@ -17,6 +17,59 @@ import java.util.UUID; public class XPipeUrlAction implements ActionProvider { + @Override + public LauncherCallSite getLauncherCallSite() { + return new XPipeLauncherCallSite() { + + @Override + public String getId() { + return "xpipe"; + } + + @Override + public Action createAction(List args) throws Exception { + switch (args.get(0)) { + case "addStore" -> { + var storeString = InPlaceSecretValue.builder() + .encryptedValue(args.get(1)) + .build(); + var store = JacksonMapper.parse(storeString.getSecretValue(), DataStore.class); + return new AddStoreAction(store); + } + case "launch" -> { + var entry = DataStorage.get() + .getStoreEntryIfPresent(UUID.fromString(args.get(1))) + .orElseThrow(); + if (!entry.getValidity().isUsable()) { + return null; + } + return new LaunchAction(entry); + } + case "action" -> { + var id = args.get(1); + ActionProvider provider = ActionProvider.ALL.stream() + .filter(actionProvider -> { + return actionProvider.getDataStoreCallSite() != null + && id.equals(actionProvider.getId()); + }) + .findFirst() + .orElseThrow(); + var entry = DataStorage.get() + .getStoreEntryIfPresent(UUID.fromString(args.get(2))) + .orElseThrow(); + if (!entry.getValidity().isUsable()) { + return null; + } + return new CallAction(provider, entry); + } + default -> { + return null; + } + } + } + }; + } + @Value static class CallAction implements ActionProvider.Action { @@ -53,7 +106,7 @@ public class XPipeUrlAction implements ActionProvider { return; } - TerminalHelper.open(storeName, command); + TerminalLauncher.open(storeName, command); } } } @@ -74,57 +127,16 @@ public class XPipeUrlAction implements ActionProvider { return; } - var entry = DataStoreEntry.createNew(UUID.randomUUID(), StoreViewState.get().getActiveCategory().getValue().getCategory().getUuid(), "", store); + var entry = DataStoreEntry.createNew( + UUID.randomUUID(), + StoreViewState.get() + .getActiveCategory() + .getValue() + .getCategory() + .getUuid(), + "", + store); StoreCreationComp.showEdit(entry); } } - - @Override - public LauncherCallSite getLauncherCallSite() { - return new XPipeLauncherCallSite() { - - @Override - public String getId() { - return "xpipe"; - } - - @Override - public Action createAction(List args) throws Exception { - switch (args.get(0)) { - case "addStore" -> { - var storeString = DefaultSecretValue.builder() - .encryptedValue(args.get(1)) - .build(); - var store = JacksonMapper.parse(storeString.getSecretValue(), DataStore.class); - return new AddStoreAction(store); - } - case "launch" -> { - var entry = DataStorage.get() - .getStoreEntryIfPresent(UUID.fromString(args.get(1))) - .orElseThrow(); - if (!entry.getValidity().isUsable()) { - return null; - } - return new LaunchAction(entry); - } - case "action" -> { - var id = args.get(1); - ActionProvider provider = ActionProvider.ALL.stream().filter(actionProvider -> { - return actionProvider.getDataStoreCallSite() != null && id.equals(actionProvider.getId()); - }).findFirst().orElseThrow(); - var entry = DataStorage.get() - .getStoreEntryIfPresent(UUID.fromString(args.get(2))) - .orElseThrow(); - if (!entry.getValidity().isUsable()) { - return null; - } - return new CallAction(provider, entry); - } - default -> { - return null; - } - } - } - }; - } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/BackAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/BackAction.java index 10c0e17d..1a3c877a 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/BackAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/BackAction.java @@ -13,30 +13,20 @@ import java.util.List; public class BackAction implements LeafAction { - public String getId() { - return "back"; - } - @Override public void execute(OpenFileSystemModel model, List entries) throws Exception { model.backSync(1); } + public String getId() { + return "back"; + } + @Override public Node getIcon(OpenFileSystemModel model, List entries) { return new FontIcon("fth-arrow-left"); } - @Override - public boolean isApplicable(OpenFileSystemModel model, List entries) { - return false; - } - - @Override - public boolean isActive(OpenFileSystemModel model, List entries) { - return model.getHistory().canGoBackProperty().get(); - } - @Override public KeyCombination getShortcut() { return new KeyCodeCombination(KeyCode.LEFT, KeyCombination.ALT_DOWN); @@ -46,4 +36,14 @@ public class BackAction implements LeafAction { public String getName(OpenFileSystemModel model, List entries) { return "Back"; } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return false; + } + + @Override + public boolean isActive(OpenFileSystemModel model, List entries) { + return model.getHistory().canGoBackProperty().get(); + } } 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 1dca415a..5dd23c05 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)); } } } @@ -56,11 +57,6 @@ public class BrowseInNativeManagerAction implements LeafAction { return true; } - @Override - public boolean isApplicable(OpenFileSystemModel model, List entries) { - return model.isLocal(); - } - @Override public String getName(OpenFileSystemModel model, List entries) { return switch (OsType.getLocal()) { @@ -69,4 +65,13 @@ public class BrowseInNativeManagerAction implements LeafAction { case OsType.MacOs macOs -> "Browse in Finder"; }; } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return model.getFileSystem() + .getShell() + .orElseThrow() + .getLocalSystemAccess() + .supportsFileSystemAccess(); + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/ChmodAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/ChmodAction.java index 5db59f4b..b64f5115 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/ChmodAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/ChmodAction.java @@ -14,8 +14,8 @@ import java.util.List; public class ChmodAction implements BranchAction { @Override - public boolean isApplicable(OpenFileSystemModel model, List entries) { - return model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS; + public Node getIcon(OpenFileSystemModel model, List entries) { + return new FontIcon("mdi2w-wrench"); } @Override @@ -24,35 +24,52 @@ public class ChmodAction implements BranchAction { } @Override - public Node getIcon(OpenFileSystemModel model, List entries) { - return new FontIcon("mdi2w-wrench"); + public String getName(OpenFileSystemModel model, List entries) { + return "Chmod"; } @Override - public String getName(OpenFileSystemModel model, List entries) { - return "Chmod"; + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS; + } + + @Override + public List getBranchingActions(OpenFileSystemModel model, List entries) { + return List.of( + new Chmod("400"), + new Chmod("600"), + new Chmod("644"), + new Chmod("700"), + new Chmod("755"), + new Chmod("777"), + new Chmod("u+x"), + new Chmod("a+x")); } private static class Chmod implements LeafAction { private final String option; - private Chmod(String option) {this.option = option;} + private Chmod(String option) { + this.option = option; + } @Override public String getName(OpenFileSystemModel model, List entries) { return option; - } + } @Override public void execute(OpenFileSystemModel model, List entries) throws Exception { - model.getFileSystem().getShell().orElseThrow().executeSimpleCommand(CommandBuilder.of().add("chmod", option) - .addFiles(entries.stream().map(browserEntry -> browserEntry.getRawFileEntry().getPath()).toList())); + model.getFileSystem() + .getShell() + .orElseThrow() + .executeSimpleCommand(CommandBuilder.of() + .add("chmod", option) + .addFiles(entries.stream() + .map(browserEntry -> + browserEntry.getRawFileEntry().getPath()) + .toList())); } } - - @Override - public List getBranchingActions(OpenFileSystemModel model, List entries) { - return List.of(new Chmod("400"), new Chmod("600"), new Chmod("644"), new Chmod("700"), new Chmod("755"), new Chmod("777"), new Chmod("u+x"), new Chmod("a+x")); - } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyAction.java index 830bd46b..084f1fac 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyAction.java @@ -21,11 +21,6 @@ public class CopyAction implements LeafAction { entries.stream().map(entry -> entry.getRawFileEntry()).toList()); } - @Override - public boolean acceptsEmptySelection() { - return true; - } - @Override public Node getIcon(OpenFileSystemModel model, List entries) { return new FontIcon("mdi2c-content-copy"); @@ -41,6 +36,11 @@ public class CopyAction implements LeafAction { return new KeyCodeCombination(KeyCode.C, KeyCombination.SHORTCUT_DOWN); } + @Override + public boolean acceptsEmptySelection() { + return true; + } + @Override public String getName(OpenFileSystemModel model, List entries) { return "Copy"; diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyPathAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyPathAction.java index 37436658..9f866a43 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyPathAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/CopyPathAction.java @@ -18,11 +18,6 @@ import java.util.stream.Collectors; public class CopyPathAction implements BrowserAction, BranchAction { - @Override - public String getName(OpenFileSystemModel model, List entries) { - return "Copy location"; - } - @Override public Category getCategory() { return Category.COPY_PASTE; @@ -33,26 +28,31 @@ public class CopyPathAction implements BrowserAction, BranchAction { return true; } + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "Copy location"; + } + @Override public List getBranchingActions(OpenFileSystemModel model, List entries) { return List.of( new LeafAction() { + @Override + public KeyCombination getShortcut() { + return new KeyCodeCombination(KeyCode.C, KeyCombination.ALT_DOWN, KeyCombination.SHORTCUT_DOWN); + } + @Override public String getName(OpenFileSystemModel model, List entries) { if (entries.size() == 1) { return " " + BrowserActionFormatter.centerEllipsis( - entries.get(0).getRawFileEntry().getPath(), 50); + entries.getFirst().getRawFileEntry().getPath(), 50); } return "Absolute Paths"; } - @Override - public KeyCombination getShortcut() { - return new KeyCodeCombination(KeyCode.C, KeyCombination.ALT_DOWN, KeyCombination.SHORTCUT_DOWN); - } - @Override public void execute(OpenFileSystemModel model, List entries) { var s = entries.stream() @@ -67,7 +67,7 @@ public class CopyPathAction implements BrowserAction, BranchAction { if (entries.size() == 1) { return " " + BrowserActionFormatter.centerEllipsis( - entries.get(0).getRawFileEntry().getPath(), 50); + entries.getFirst().getRawFileEntry().getPath(), 50); } return "Absolute Link Paths"; @@ -99,7 +99,7 @@ public class CopyPathAction implements BrowserAction, BranchAction { if (entries.size() == 1) { return "\"" + BrowserActionFormatter.centerEllipsis( - entries.get(0).getRawFileEntry().getPath(), 50) + entries.getFirst().getRawFileEntry().getPath(), 50) + "\""; } @@ -122,12 +122,18 @@ public class CopyPathAction implements BrowserAction, BranchAction { } }, new LeafAction() { + @Override + public KeyCombination getShortcut() { + return new KeyCodeCombination( + KeyCode.C, KeyCombination.SHIFT_DOWN, KeyCombination.SHORTCUT_DOWN); + } + @Override public String getName(OpenFileSystemModel model, List entries) { if (entries.size() == 1) { return " " + BrowserActionFormatter.centerEllipsis( - FileNames.getFileName(entries.get(0) + FileNames.getFileName(entries.getFirst() .getRawFileEntry() .getPath()), 50); @@ -136,12 +142,6 @@ public class CopyPathAction implements BrowserAction, BranchAction { return "File Names"; } - @Override - public KeyCombination getShortcut() { - return new KeyCodeCombination( - KeyCode.C, KeyCombination.SHIFT_DOWN, KeyCombination.SHORTCUT_DOWN); - } - @Override public void execute(OpenFileSystemModel model, List entries) { var s = entries.stream() @@ -157,7 +157,7 @@ public class CopyPathAction implements BrowserAction, BranchAction { if (entries.size() == 1) { return " " + BrowserActionFormatter.centerEllipsis( - FileNames.getFileName(entries.get(0) + FileNames.getFileName(entries.getFirst() .getRawFileEntry() .getPath()), 50); @@ -199,7 +199,7 @@ public class CopyPathAction implements BrowserAction, BranchAction { if (entries.size() == 1) { return "\"" + BrowserActionFormatter.centerEllipsis( - FileNames.getFileName(entries.get(0) + FileNames.getFileName(entries.getFirst() .getRawFileEntry() .getPath()), 50) diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/DeleteAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/DeleteAction.java index 1ed2e48e..f2b1fa34 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/DeleteAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/DeleteAction.java @@ -28,13 +28,13 @@ public class DeleteAction implements LeafAction { } @Override - public Category getCategory() { - return Category.MUTATION; + public Node getIcon(OpenFileSystemModel model, List entries) { + return new FontIcon("mdi2d-delete"); } @Override - public Node getIcon(OpenFileSystemModel model, List entries) { - return new FontIcon("mdi2d-delete"); + public Category getCategory() { + return Category.MUTATION; } @Override @@ -44,6 +44,11 @@ public class DeleteAction implements LeafAction { @Override public String getName(OpenFileSystemModel model, List entries) { - return "Delete" + (entries.stream().allMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.LINK) ? " link" : ""); + return "Delete" + + (entries.stream() + .allMatch(browserEntry -> + browserEntry.getRawFileEntry().getKind() == FileKind.LINK) + ? " link" + : ""); } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/DeleteLinkAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/DeleteLinkAction.java new file mode 100644 index 00000000..47489c04 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/DeleteLinkAction.java @@ -0,0 +1,46 @@ +package io.xpipe.ext.base.browser; + +import io.xpipe.app.browser.BrowserEntry; +import io.xpipe.app.browser.FileSystemHelper; +import io.xpipe.app.browser.OpenFileSystemModel; +import io.xpipe.app.browser.action.LeafAction; +import io.xpipe.core.store.FileKind; +import javafx.scene.Node; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.util.List; + +public class DeleteLinkAction implements LeafAction { + + @Override + public boolean automaticallyResolveLinks() { + return false; + } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return entries.stream().allMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.LINK); + } + + @Override + public void execute(OpenFileSystemModel model, List entries) throws Exception { + var toDelete = entries.stream().map(entry -> entry.getRawFileEntry()).toList(); + FileSystemHelper.delete(toDelete); + model.refreshSync(); + } + + @Override + public Node getIcon(OpenFileSystemModel model, List entries) { + return new FontIcon("mdi2d-delete"); + } + + @Override + public Category getCategory() { + return Category.MUTATION; + } + + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "Delete link"; + } +} 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 abff2fd1..84591c68 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 @@ -20,14 +20,20 @@ public class EditFileAction implements LeafAction { } } + @Override + public Node getIcon(OpenFileSystemModel model, List entries) { + return new FontIcon("mdi2p-pencil"); + } + @Override public Category getCategory() { return Category.OPEN; } @Override - public Node getIcon(OpenFileSystemModel model, List entries) { - return new FontIcon("mdi2p-pencil"); + public String getName(OpenFileSystemModel model, List entries) { + var e = AppPrefs.get().externalEditor().getValue(); + return "Edit with " + (e != null ? e.toTranslatedString().getValue() : "?"); } @Override @@ -40,10 +46,4 @@ public class EditFileAction implements LeafAction { var e = AppPrefs.get().externalEditor().getValue(); return e != null; } - - @Override - public String getName(OpenFileSystemModel model, List entries) { - var e = AppPrefs.get().externalEditor().getValue(); - return "Edit with " + (e != null ? e.toTranslatedString() : "?"); - } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/FileTypeAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/FileTypeAction.java index d0f8df9c..27df7389 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/FileTypeAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/FileTypeAction.java @@ -11,16 +11,16 @@ import java.util.List; public interface FileTypeAction extends BrowserAction { + @Override + default Node getIcon(OpenFileSystemModel model, List entries) { + return BrowserIcons.createIcon(getType()).createRegion(); + } + @Override default boolean isApplicable(OpenFileSystemModel model, List entries) { var t = getType(); return entries.stream().allMatch(entry -> t.matches(entry.getRawFileEntry())); } - @Override - default Node getIcon(OpenFileSystemModel model, List entries) { - return BrowserIcons.createIcon(getType()).createRegion(); - } - FileType getType(); } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/FollowLinkAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/FollowLinkAction.java index eff061a9..337f7774 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/FollowLinkAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/FollowLinkAction.java @@ -3,8 +3,8 @@ package io.xpipe.ext.base.browser; import io.xpipe.app.browser.BrowserEntry; import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.browser.action.LeafAction; -import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileKind; +import io.xpipe.core.store.FileNames; import javafx.scene.Node; import org.kordamp.ikonli.javafx.FontIcon; @@ -12,35 +12,38 @@ import java.util.List; public class FollowLinkAction implements LeafAction { - @Override - public boolean automaticallyResolveLinks() { - return false; - } - @Override public void execute(OpenFileSystemModel model, List entries) { - var target = FileNames.getParent(entries.get(0).getRawFileEntry().resolved().getPath()); + var target = FileNames.getParent( + entries.getFirst().getRawFileEntry().resolved().getPath()); model.cdAsync(target); } - @Override - public Category getCategory() { - return Category.OPEN; - } - @Override public Node getIcon(OpenFileSystemModel model, List entries) { return new FontIcon("mdi2a-arrow-top-right-thick"); } @Override - public boolean isApplicable(OpenFileSystemModel model, List entries) { - return entries.size() == 1 - && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.LINK && entry.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY); + public Category getCategory() { + return Category.OPEN; } @Override public String getName(OpenFileSystemModel model, List entries) { return "Follow link"; } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return entries.size() == 1 + && entries.stream() + .allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.LINK + && entry.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY); + } + + @Override + public boolean automaticallyResolveLinks() { + return false; + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/ForwardAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/ForwardAction.java index e49c0ec2..6179ac70 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/ForwardAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/ForwardAction.java @@ -13,30 +13,20 @@ import java.util.List; public class ForwardAction implements LeafAction { - public String getId() { - return "forward"; - } - @Override public void execute(OpenFileSystemModel model, List entries) throws Exception { model.forthSync(1); } + public String getId() { + return "forward"; + } + @Override public Node getIcon(OpenFileSystemModel model, List entries) { return new FontIcon("fth-arrow-right"); } - @Override - public boolean isApplicable(OpenFileSystemModel model, List entries) { - return false; - } - - @Override - public boolean isActive(OpenFileSystemModel model, List entries) { - return model.getHistory().canGoForthProperty().get(); - } - @Override public KeyCombination getShortcut() { return new KeyCodeCombination(KeyCode.RIGHT, KeyCombination.ALT_DOWN); @@ -46,4 +36,14 @@ public class ForwardAction implements LeafAction { public String getName(OpenFileSystemModel model, List entries) { return "Forward"; } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return false; + } + + @Override + public boolean isActive(OpenFileSystemModel model, List entries) { + return model.getHistory().canGoForthProperty().get(); + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/JarAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/JarAction.java index 5c329b40..697a1315 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/JarAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/JarAction.java @@ -16,6 +16,11 @@ public class JarAction extends MultiExecuteAction implements JavaAction, FileTyp return Category.CUSTOM; } + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "java -jar " + BrowserActionFormatter.filesArgument(entries); + } + @Override public boolean isApplicable(OpenFileSystemModel model, List entries) { return super.isApplicable(model, entries) && FileTypeAction.super.isApplicable(model, entries); @@ -26,11 +31,6 @@ public class JarAction extends MultiExecuteAction implements JavaAction, FileTyp return "java -jar " + entry.getOptionallyQuotedFileName(); } - @Override - public String getName(OpenFileSystemModel model, List entries) { - return "java -jar " + BrowserActionFormatter.filesArgument(entries); - } - @Override public FileType getType() { return FileType.byId("jar"); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/JavapAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/JavapAction.java index 7804c06f..f23446ba 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/JavapAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/JavapAction.java @@ -16,13 +16,13 @@ public class JavapAction extends ToFileCommandAction implements FileTypeAction, } @Override - public boolean isApplicable(OpenFileSystemModel model, List entries) { - return super.isApplicable(model, entries) && FileTypeAction.super.isApplicable(model, entries); + public String getName(OpenFileSystemModel model, List entries) { + return "javap -c -p " + BrowserActionFormatter.filesArgument(entries); } @Override - public String getName(OpenFileSystemModel model, List entries) { - return "javap -c -p " + BrowserActionFormatter.filesArgument(entries); + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return super.isApplicable(model, entries) && FileTypeAction.super.isApplicable(model, entries); } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/NewItemAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/NewItemAction.java index 5a79e277..2424a2fd 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/NewItemAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/NewItemAction.java @@ -25,8 +25,8 @@ public class NewItemAction implements BrowserAction, BranchAction { } @Override - public String getName(OpenFileSystemModel model, List entries) { - return "New"; + public Category getCategory() { + return Category.MUTATION; } @Override @@ -34,29 +34,24 @@ public class NewItemAction implements BrowserAction, BranchAction { return true; } + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "New"; + } + @Override public boolean isApplicable(OpenFileSystemModel model, List entries) { return entries.size() == 1 - && entries.get(0) + && entries.getFirst() .getRawFileEntry() .getPath() .equals(model.getCurrentPath().get()); } - @Override - public Category getCategory() { - return Category.MUTATION; - } - @Override public List getBranchingActions(OpenFileSystemModel model, List entries) { return List.of( new LeafAction() { - @Override - public String getName(OpenFileSystemModel model, List entries) { - return "File"; - } - @Override public void execute(OpenFileSystemModel model, List entries) { var name = new SimpleStringProperty(); @@ -78,13 +73,13 @@ public class NewItemAction implements BrowserAction, BranchAction { public Node getIcon(OpenFileSystemModel model, List entries) { return BrowserIcons.createDefaultFileIcon().createRegion(); } - }, - new LeafAction() { + @Override public String getName(OpenFileSystemModel model, List entries) { - return "Directory"; + return "File"; } - + }, + new LeafAction() { @Override public void execute(OpenFileSystemModel model, List entries) { var name = new SimpleStringProperty(); @@ -106,18 +101,13 @@ public class NewItemAction implements BrowserAction, BranchAction { public Node getIcon(OpenFileSystemModel model, List entries) { return BrowserIcons.createDefaultDirectoryIcon().createRegion(); } - }, - new LeafAction() { + @Override public String getName(OpenFileSystemModel model, List entries) { - return "Symbolic link"; + return "Directory"; } - - @Override - public boolean isApplicable(OpenFileSystemModel model, List entries) { - return model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS; - } - + }, + new LeafAction() { @Override public void execute(OpenFileSystemModel model, List entries) { var linkName = new SimpleStringProperty(); @@ -145,6 +135,16 @@ public class NewItemAction implements BrowserAction, BranchAction { public Node getIcon(OpenFileSystemModel model, List entries) { return BrowserIcons.createDefaultFileIcon().createRegion(); } + + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "Symbolic link"; + } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS; + } }); } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryAction.java index 398db85b..f1b9e60d 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryAction.java @@ -16,12 +16,7 @@ public class OpenDirectoryAction implements LeafAction { @Override public void execute(OpenFileSystemModel model, List entries) { - model.cdAsync(entries.get(0).getRawFileEntry().getPath()); - } - - @Override - public Category getCategory() { - return Category.OPEN; + model.cdAsync(entries.getFirst().getRawFileEntry().getPath()); } @Override @@ -30,9 +25,8 @@ public class OpenDirectoryAction implements LeafAction { } @Override - public boolean isApplicable(OpenFileSystemModel model, List entries) { - return entries.size() == 1 - && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY); + public Category getCategory() { + return Category.OPEN; } @Override @@ -44,4 +38,10 @@ public class OpenDirectoryAction implements LeafAction { public String getName(OpenFileSystemModel model, List entries) { return "Open"; } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return entries.size() == 1 + && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY); + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryInNewTabAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryInNewTabAction.java index 458cd32a..4c00907f 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryInNewTabAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenDirectoryInNewTabAction.java @@ -16,24 +16,18 @@ public class OpenDirectoryInNewTabAction implements LeafAction { model.getBrowserModel() .openFileSystemAsync( model.getEntry(), - m -> entries.get(0).getRawFileEntry().getPath(), + m -> entries.getFirst().getRawFileEntry().getPath(), null); } - @Override - public Category getCategory() { - return Category.OPEN; - } - @Override public Node getIcon(OpenFileSystemModel model, List entries) { return new FontIcon("mdi2f-folder-open-outline"); } @Override - public boolean isApplicable(OpenFileSystemModel model, List entries) { - return entries.size() == 1 - && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY); + public Category getCategory() { + return Category.OPEN; } @Override @@ -45,4 +39,10 @@ public class OpenDirectoryInNewTabAction implements LeafAction { public String getName(OpenFileSystemModel model, List entries) { return "Open in new tab"; } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return entries.size() == 1 + && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY); + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileDefaultAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileDefaultAction.java index b0702f85..f56495a1 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileDefaultAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenFileDefaultAction.java @@ -22,19 +22,14 @@ public class OpenFileDefaultAction implements LeafAction { } } - @Override - public Category getCategory() { - return Category.OPEN; - } - @Override public Node getIcon(OpenFileSystemModel model, List entries) { return new FontIcon("mdi2b-book-open-variant"); } @Override - public boolean isApplicable(OpenFileSystemModel model, List entries) { - return entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE); + public Category getCategory() { + return Category.OPEN; } @Override @@ -46,4 +41,9 @@ public class OpenFileDefaultAction implements LeafAction { public String getName(OpenFileSystemModel model, List entries) { return "Open with default application"; } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE); + } } 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 ccb8c594..832126fd 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; @@ -20,33 +17,9 @@ import java.util.List; 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(); - } - } - } - - @Override - public Category getCategory() { - return Category.OPEN; + public void execute(OpenFileSystemModel model, List entries) { + var e = entries.getFirst(); + FileOpener.openWithAnyApplication(e.getRawFileEntry()); } @Override @@ -55,12 +28,8 @@ 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) - && entries.size() == 1 - && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE); + public Category getCategory() { + return Category.OPEN; } @Override @@ -72,4 +41,11 @@ public class OpenFileWithAction implements LeafAction { public String getName(OpenFileSystemModel model, List entries) { return "Open with ..."; } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return OsType.getLocal().equals(OsType.WINDOWS) + && 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 3679931a..c30caaf9 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 @@ -4,9 +4,9 @@ import io.xpipe.app.browser.BrowserEntry; import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.browser.action.LeafAction; import io.xpipe.app.util.LocalShell; -import io.xpipe.core.store.FileNames; import io.xpipe.core.process.OsType; import io.xpipe.core.process.ShellControl; +import io.xpipe.core.store.FileNames; import java.util.List; @@ -17,62 +17,66 @@ 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. // So let's keep one process running - LocalShell.getLocalPowershell().command(content).notComplex().execute(); + LocalShell.getLocalPowershell() + .command(content) + .notComplex() + .execute(); } case OsType.Linux linux -> { var dbus = String.format( """ 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 + tell application "Finder" to open information window of alias fileEntry """, - entry.getRawFileEntry().getPath())) + localFile)) .execute(); } } } } - @Override - public boolean acceptsEmptySelection() { - return true; - } - @Override public Category getCategory() { return Category.NATIVE; } @Override - public boolean isApplicable(OpenFileSystemModel model, List entries) { - var sc = model.getFileSystem().getShell(); - return model.isLocal(); + public boolean acceptsEmptySelection() { + return true; } @Override public String getName(OpenFileSystemModel model, List entries) { return "Show details"; } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + var sc = model.getFileSystem().getShell().orElseThrow(); + return sc.getLocalSystemAccess().supportsFileSystemAccess(); + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java index a5f773cd..cdfe1e67 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java @@ -15,14 +15,13 @@ import java.util.List; public class OpenTerminalAction implements LeafAction { - public String getId() { - return "openTerminal"; - } - @Override - public void execute(OpenFileSystemModel model, List entries) throws Exception { + public void execute(OpenFileSystemModel model, List entries) { if (entries.size() == 0) { - model.openTerminalAsync(model.getCurrentDirectory() != null ? model.getCurrentDirectory().getPath() : null); + model.openTerminalAsync( + model.getCurrentDirectory() != null + ? model.getCurrentDirectory().getPath() + : null); return; } @@ -31,14 +30,28 @@ public class OpenTerminalAction implements LeafAction { } } + public String getId() { + return "openTerminal"; + } + + @Override + public Node getIcon(OpenFileSystemModel model, List entries) { + return new FontIcon("mdi2c-console"); + } + @Override public Category getCategory() { return Category.OPEN; } @Override - public Node getIcon(OpenFileSystemModel model, List entries) { - return new FontIcon("mdi2c-console"); + public KeyCombination getShortcut() { + return new KeyCodeCombination(KeyCode.T, KeyCombination.SHORTCUT_DOWN); + } + + @Override + public String getName(OpenFileSystemModel model, List entries) { + return "Open in terminal"; } @Override @@ -51,14 +64,4 @@ public class OpenTerminalAction implements LeafAction { var t = AppPrefs.get().terminalType().getValue(); return t != null; } - - @Override - public KeyCombination getShortcut() { - return new KeyCodeCombination(KeyCode.T, KeyCombination.SHORTCUT_DOWN); - } - - @Override - public String getName(OpenFileSystemModel model, List entries) { - return "Open in terminal"; - } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/PasteAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/PasteAction.java index 3c4617c6..220a7c23 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/PasteAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/PasteAction.java @@ -22,8 +22,8 @@ public class PasteAction implements LeafAction { return; } - var target = entries.size() == 1 && entries.get(0).getRawFileEntry().getKind() == FileKind.DIRECTORY - ? entries.get(0).getRawFileEntry() + var target = entries.size() == 1 && entries.getFirst().getRawFileEntry().getKind() == FileKind.DIRECTORY + ? entries.getFirst().getRawFileEntry() : model.getCurrentDirectory(); var files = clipboard.getEntries(); if (files.size() == 0) { @@ -33,24 +33,14 @@ public class PasteAction implements LeafAction { model.dropFilesIntoAsync(target, files, true); } - @Override - public Category getCategory() { - return Category.COPY_PASTE; - } - @Override public Node getIcon(OpenFileSystemModel model, List entries) { return new FontIcon("mdi2c-content-paste"); } @Override - public boolean isApplicable(OpenFileSystemModel model, List entries) { - return (entries.size() == 1 && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY)) || entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE); - } - - @Override - public boolean isActive(OpenFileSystemModel model, List entries) { - return BrowserClipboard.retrieveCopy() != null; + public Category getCategory() { + return Category.COPY_PASTE; } @Override @@ -67,4 +57,17 @@ public class PasteAction implements LeafAction { public String getName(OpenFileSystemModel model, List entries) { return "Paste"; } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return (entries.size() == 1 + && entries.stream() + .allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY)) + || entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE); + } + + @Override + public boolean isActive(OpenFileSystemModel model, List entries) { + return BrowserClipboard.retrieveCopy() != null; + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/RefreshDirectoryAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/RefreshDirectoryAction.java index 070ccaad..151007c7 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/RefreshDirectoryAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/RefreshDirectoryAction.java @@ -13,18 +13,13 @@ import java.util.List; public class RefreshDirectoryAction implements LeafAction { - public String getId() { - return "refresh"; - } - @Override public void execute(OpenFileSystemModel model, List entries) throws Exception { model.refreshSync(); } - @Override - public boolean isActive(OpenFileSystemModel model, List entries) { - return !model.getInOverview().get(); + public String getId() { + return "refresh"; } @Override @@ -32,11 +27,6 @@ public class RefreshDirectoryAction implements LeafAction { return new FontIcon("mdmz-refresh"); } - @Override - public boolean isApplicable(OpenFileSystemModel model, List entries) { - return false; - } - @Override public KeyCombination getShortcut() { return new KeyCodeCombination(KeyCode.F5); @@ -46,4 +36,14 @@ public class RefreshDirectoryAction implements LeafAction { public String getName(OpenFileSystemModel model, List entries) { return "Refresh"; } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return false; + } + + @Override + public boolean isActive(OpenFileSystemModel model, List entries) { + return !model.getInOverview().get(); + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/RenameAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/RenameAction.java index b2bf50dd..416e4e34 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/RenameAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/RenameAction.java @@ -16,17 +16,7 @@ public class RenameAction implements LeafAction { @Override public void execute(OpenFileSystemModel model, List entries) { - model.getFileList().getEditing().setValue(entries.get(0)); - } - - @Override - public boolean automaticallyResolveLinks() { - return false; - } - - @Override - public Category getCategory() { - return Category.MUTATION; + model.getFileList().getEditing().setValue(entries.getFirst()); } @Override @@ -35,8 +25,8 @@ public class RenameAction implements LeafAction { } @Override - public boolean isApplicable(OpenFileSystemModel model, List entries) { - return entries.size() == 1 && entries.get(0).getRawFileEntry().getKind() != FileKind.LINK; + public Category getCategory() { + return Category.MUTATION; } @Override @@ -48,4 +38,14 @@ public class RenameAction implements LeafAction { public String getName(OpenFileSystemModel model, List entries) { return "Rename"; } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return entries.size() == 1 && entries.getFirst().getRawFileEntry().getKind() != FileKind.LINK; + } + + @Override + public boolean automaticallyResolveLinks() { + return false; + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/RunAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/RunAction.java index bc58933c..3e87786e 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/RunAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/RunAction.java @@ -44,13 +44,13 @@ public class RunAction extends MultiExecuteAction { } @Override - public Category getCategory() { - return Category.CUSTOM; + public Node getIcon(OpenFileSystemModel model, List entries) { + return new FontIcon("mdi2p-play"); } @Override - public Node getIcon(OpenFileSystemModel model, List entries) { - return new FontIcon("mdi2p-play"); + public Category getCategory() { + return Category.CUSTOM; } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/UnzipAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/UnzipAction.java index 8801065e..50a78730 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/UnzipAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/UnzipAction.java @@ -4,8 +4,8 @@ import io.xpipe.app.browser.BrowserEntry; import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.browser.action.ExecuteApplicationAction; import io.xpipe.app.browser.icon.FileType; -import io.xpipe.core.store.FileNames; import io.xpipe.core.process.OsType; +import io.xpipe.core.store.FileNames; import java.util.List; @@ -17,9 +17,8 @@ public class UnzipAction extends ExecuteApplicationAction implements FileTypeAct } @Override - public boolean isApplicable(OpenFileSystemModel model, List entries) { - return FileTypeAction.super.isApplicable(model, entries) - && !model.getFileSystem().getShell().orElseThrow().getOsType().equals(OsType.WINDOWS); + protected boolean refresh() { + return true; } @Override @@ -28,11 +27,6 @@ public class UnzipAction extends ExecuteApplicationAction implements FileTypeAct + FileNames.quoteIfNecessary(FileNames.getBaseName(entry.getFileName())); } - @Override - protected boolean refresh() { - return true; - } - @Override public Category getCategory() { return Category.CUSTOM; @@ -43,6 +37,12 @@ public class UnzipAction extends ExecuteApplicationAction implements FileTypeAct return "unzip [...]"; } + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return FileTypeAction.super.isApplicable(model, entries) + && !model.getFileSystem().getShell().orElseThrow().getOsType().equals(OsType.WINDOWS); + } + @Override public FileType getType() { return FileType.byId("zip"); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/PredefinedScriptStore.java b/ext/base/src/main/java/io/xpipe/ext/base/script/PredefinedScriptStore.java index 586739eb..89d6c54c 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/PredefinedScriptStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/PredefinedScriptStore.java @@ -14,26 +14,21 @@ import java.util.function.Supplier; @Getter public enum PredefinedScriptStore { - CLINK_SETUP( - "Clink Setup", - () -> SimpleScriptStore.builder() - .group(PredefinedScriptGroup.CLINK.getEntry()) - .minimumDialect(ShellDialects.CMD) - .commands(file("clink.bat")) - .executionType(SimpleScriptStore.ExecutionType.TERMINAL_ONLY) - .build()), - CLINK_INJECT( - "Clink Inject", - () -> SimpleScriptStore.builder() - .group(PredefinedScriptGroup.CLINK.getEntry()) - .minimumDialect(ShellDialects.CMD) - .script(CLINK_SETUP.getEntry()) - .commands( - """ + CLINK_SETUP("Clink Setup", () -> SimpleScriptStore.builder() + .group(PredefinedScriptGroup.CLINK.getEntry()) + .minimumDialect(ShellDialects.CMD) + .commands(file("clink.bat")) + .executionType(SimpleScriptStore.ExecutionType.TERMINAL_ONLY) + .build()), + CLINK_INJECT("Clink Inject", () -> SimpleScriptStore.builder() + .group(PredefinedScriptGroup.CLINK.getEntry()) + .minimumDialect(ShellDialects.CMD) + .script(CLINK_SETUP.getEntry()) + .commands(""" clink inject --quiet """) - .executionType(SimpleScriptStore.ExecutionType.TERMINAL_ONLY) - .build()), + .executionType(SimpleScriptStore.ExecutionType.TERMINAL_ONLY) + .build()), STARSHIP_BASH("Starship Bash", () -> SimpleScriptStore.builder() .group(PredefinedScriptGroup.STARSHIP.getEntry()) .minimumDialect(ShellDialects.BASH) @@ -52,31 +47,19 @@ public enum PredefinedScriptStore { .commands(file("starship_fish.fish")) .executionType(SimpleScriptStore.ExecutionType.TERMINAL_ONLY) .build()), - STARSHIP_CMD( - "Starship Cmd", - () -> SimpleScriptStore.builder() - .group(PredefinedScriptGroup.STARSHIP.getEntry()) - .minimumDialect(ShellDialects.CMD) - .script(CLINK_SETUP.getEntry()) - .commands(file(("starship_cmd.bat"))) - .executionType(SimpleScriptStore.ExecutionType.TERMINAL_ONLY) - .build()), - STARSHIP_POWERSHELL( - "Starship Powershell", - () -> SimpleScriptStore.builder() - .group(PredefinedScriptGroup.STARSHIP.getEntry()) - .minimumDialect(ShellDialects.POWERSHELL) - .commands(file("starship_powershell.ps1")) - .executionType(SimpleScriptStore.ExecutionType.TERMINAL_ONLY) - .build()); - - public static String file(String name) { - AtomicReference string = new AtomicReference<>(); - AppResources.with("io.xpipe.ext.base", "scripts/" + name, var1 -> { - string.set(Files.readString(var1)); - }); - return string.get(); - } + STARSHIP_CMD("Starship Cmd", () -> SimpleScriptStore.builder() + .group(PredefinedScriptGroup.STARSHIP.getEntry()) + .minimumDialect(ShellDialects.CMD) + .script(CLINK_SETUP.getEntry()) + .commands(file(("starship_cmd.bat"))) + .executionType(SimpleScriptStore.ExecutionType.TERMINAL_ONLY) + .build()), + STARSHIP_POWERSHELL("Starship Powershell", () -> SimpleScriptStore.builder() + .group(PredefinedScriptGroup.STARSHIP.getEntry()) + .minimumDialect(ShellDialects.POWERSHELL) + .commands(file("starship_powershell.ps1")) + .executionType(SimpleScriptStore.ExecutionType.TERMINAL_ONLY) + .build()); private final String name; private final Supplier scriptStore; @@ -90,4 +73,12 @@ public enum PredefinedScriptStore { this.scriptStore = scriptStore; this.uuid = UUID.nameUUIDFromBytes(name.getBytes(StandardCharsets.UTF_8)); } + + public static String file(String name) { + AtomicReference string = new AtomicReference<>(); + AppResources.with("io.xpipe.ext.base", "scripts/" + name, var1 -> { + string.set(Files.readString(var1)); + }); + return string.get(); + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStoreProvider.java index 7e61939d..ec297756 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStoreProvider.java @@ -47,6 +47,22 @@ public class ScriptGroupStoreProvider implements DataStoreProvider { return new DenseStoreEntryComp(sec.getWrapper(), true, dropdown); } + @Override + public Comp stateDisplay(StoreEntryWrapper w) { + return new SystemStateComp(new SimpleObjectProperty<>(SystemStateComp.State.SUCCESS)); + } + + @Override + public CreationCategory getCreationCategory() { + return CreationCategory.SCRIPT; + } + + @Override + public DataStoreEntry getDisplayParent(DataStoreEntry store) { + ScriptGroupStore scriptStore = store.getStore().asNeeded(); + return scriptStore.getParent() != null ? scriptStore.getParent().get() : null; + } + @SneakyThrows @Override public GuiDialog guiDialog(DataStoreEntry entry, Property store) { @@ -62,7 +78,12 @@ public class ScriptGroupStoreProvider implements DataStoreProvider { .description("scriptGroupDescription") .addComp( new DataStoreChoiceComp<>( - DataStoreChoiceComp.Mode.OTHER, entry, group, ScriptGroupStore.class, null, StoreViewState.get().getAllScriptsCategory()), + DataStoreChoiceComp.Mode.OTHER, + entry, + group, + ScriptGroupStore.class, + null, + StoreViewState.get().getAllScriptsCategory()), group) .bind( () -> { @@ -76,9 +97,14 @@ public class ScriptGroupStoreProvider implements DataStoreProvider { } @Override - public DataStoreEntry getDisplayParent(DataStoreEntry store) { - ScriptGroupStore scriptStore = store.getStore().asNeeded(); - return scriptStore.getParent() != null ? scriptStore.getParent().get() : null; + public ObservableValue informationString(StoreEntryWrapper wrapper) { + ScriptGroupStore scriptStore = wrapper.getEntry().getStore().asNeeded(); + return new SimpleStringProperty(scriptStore.getDescription()); + } + + @Override + public String getDisplayIconFileName(DataStore store) { + return "proc:shellEnvironment_icon.svg"; } @Override @@ -86,27 +112,6 @@ public class ScriptGroupStoreProvider implements DataStoreProvider { return ScriptGroupStore.builder().build(); } - @Override - public Comp stateDisplay(StoreEntryWrapper w) { - return new SystemStateComp(new SimpleObjectProperty<>(SystemStateComp.State.SUCCESS)); - } - - @Override - public CreationCategory getCreationCategory() { - return CreationCategory.SCRIPT; - } - - @Override - public String getDisplayIconFileName(DataStore store) { - return "proc:shellEnvironment_icon.svg"; - } - - @Override - public ObservableValue informationString(StoreEntryWrapper wrapper) { - ScriptGroupStore scriptStore = wrapper.getEntry().getStore().asNeeded(); - return new SimpleStringProperty(scriptStore.getDescription()); - } - @Override public List getPossibleNames() { return List.of("scriptGroup"); @@ -116,5 +121,4 @@ public class ScriptGroupStoreProvider implements DataStoreProvider { public List> getStoreClasses() { return List.of(ScriptGroupStore.class); } - } 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 442ba63f..f688f5a7 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 @@ -4,6 +4,7 @@ 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.app.util.ShellTemp; import io.xpipe.app.util.Validators; import io.xpipe.core.process.ScriptSnippet; import io.xpipe.core.process.ShellControl; @@ -13,7 +14,6 @@ import io.xpipe.core.store.DataStoreState; import io.xpipe.core.store.FileNames; import io.xpipe.core.store.StatefulDataStore; import io.xpipe.core.util.JacksonizedValue; -import io.xpipe.core.util.XPipeInstallation; import lombok.*; import lombok.experimental.FieldDefaults; import lombok.experimental.SuperBuilder; @@ -26,12 +26,27 @@ import java.util.*; @AllArgsConstructor public abstract class ScriptStore extends JacksonizedValue implements DataStore, StatefulDataStore { + protected final DataStoreEntryRef group; + + @Singular + protected final List> scripts; + + protected final String description; + public static ShellControl controlWithDefaultScripts(ShellControl pc) { return controlWithScripts(pc, getDefaultInitScripts(), getDefaultBringScripts()); } - public static ShellControl controlWithScripts(ShellControl pc, List> initScripts, List> bringScripts) { + public static ShellControl controlWithScripts( + ShellControl pc, + List> initScripts, + List> bringScripts) { try { + // Don't copy scripts if we don't want to modify the file system + if (!pc.getEffectiveSecurityPolicy().permitTempScriptCreation()) { + return pc; + } + var initFlattened = flatten(initScripts); var bringFlattened = flatten(bringScripts); @@ -45,17 +60,20 @@ 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)); } }); return pc; + } catch (StackOverflowError t) { + throw new RuntimeException("Unable to set up scripts. Is there a circular script dependency?", t); } catch (Throwable t) { throw new RuntimeException("Unable to set up scripts", t); } } - private static void passInitScripts(ShellControl pc, List scriptStores) throws Exception { + private static void passInitScripts(ShellControl pc, List scriptStores) { scriptStores.forEach(simpleScriptStore -> { if (pc.getInitCommands().contains(simpleScriptStore)) { return; @@ -69,22 +87,34 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore, }); } - private static String initScriptsDirectory(ShellControl proc, List scriptStores) throws Exception { + private static String initScriptsDirectory(ShellControl proc, List scriptStores) + throws Exception { if (scriptStores.isEmpty()) { return null; } - var applicable = scriptStores.stream().filter(simpleScriptStore -> simpleScriptStore.getMinimumDialect().isCompatibleTo(proc.getShellDialect())).toList(); + var applicable = scriptStores.stream() + .filter(simpleScriptStore -> + simpleScriptStore.getMinimumDialect().isCompatibleTo(proc.getShellDialect())) + .toList(); if (applicable.isEmpty()) { return null; } - var refs = applicable.stream().map(scriptStore -> { - return DataStorage.get().getStoreEntries().stream().filter(dataStoreEntry -> dataStoreEntry.getStore() == scriptStore).findFirst().map(entry -> entry.ref()); - }).flatMap(Optional::stream).toList(); - var hash = refs.stream().mapToInt(value -> value.get().getName().hashCode() + value.getStore().hashCode()).sum(); - var xpipeHome = XPipeInstallation.getDataDir(proc); - var targetDir = FileNames.join(xpipeHome, "scripts", proc.getShellDialect().getId()); + var refs = applicable.stream() + .map(scriptStore -> { + return DataStorage.get().getStoreEntries().stream() + .filter(dataStoreEntry -> dataStoreEntry.getStore() == scriptStore) + .findFirst() + .map(entry -> entry.ref()); + }) + .flatMap(Optional::stream) + .toList(); + var hash = refs.stream() + .mapToInt(value -> + value.get().getName().hashCode() + value.getStore().hashCode()) + .sum(); + var targetDir = FileNames.join(ShellTemp.getUserSpecificTempDataDirectory(proc,"scripts"), proc.getShellDialect().getId()); var hashFile = FileNames.join(targetDir, "hash"); var d = proc.getShellDialect(); if (d.createFileExistsCommand(proc, hashFile).executeAndCheck()) { @@ -158,12 +188,33 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore, return sorted; } - protected final DataStoreEntryRef group; + @Override + public Class getStateClass() { + return State.class; + } - @Singular - protected final List> scripts; + @Override + public void checkComplete() throws Throwable { + Validators.isType(group, ScriptGroupStore.class); + if (scripts != null) { + Validators.contentNonNull(scripts); + } - protected final String description; + // Prevent possible stack overflow + // for (DataStoreEntryRef s : getEffectiveScripts()) { + // s.checkComplete(); + // } + } + + SequencedCollection queryFlattenedScripts() { + var seen = new LinkedHashSet(); + queryFlattenedScripts(seen); + return seen; + } + + protected abstract void queryFlattenedScripts(LinkedHashSet all); + + public abstract List> getEffectiveScripts(); @FieldDefaults(level = AccessLevel.PRIVATE) @Setter @@ -181,32 +232,4 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore, bringToShell = s.bringToShell; } } - - @Override - public Class getStateClass() { - return State.class; - } - - @Override - public void checkComplete() throws Throwable { - Validators.isType(group, ScriptGroupStore.class); - if (scripts != null) { - Validators.contentNonNull(scripts); - } - - // Prevent possible stack overflow -// for (DataStoreEntryRef s : getEffectiveScripts()) { -// s.checkComplete(); -// } - } - - SequencedCollection queryFlattenedScripts() { - var seen = new LinkedHashSet(); - queryFlattenedScripts(seen); - return seen; - } - - protected abstract void queryFlattenedScripts(LinkedHashSet all); - - public abstract List> getEffectiveScripts(); } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStoreTypeChoiceComp.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStoreTypeChoiceComp.java index 0f2b2743..b8eee04c 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStoreTypeChoiceComp.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStoreTypeChoiceComp.java @@ -26,7 +26,6 @@ public class ScriptStoreTypeChoiceComp extends SimpleComp { Arrays.stream(available).forEach(executionType -> { map.put(executionType, AppI18n.observable(executionType.getId())); }); - return new ToggleGroupComp<>(selected, new SimpleObjectProperty<>(map)) - .createRegion(); + return new ToggleGroupComp<>(selected, new SimpleObjectProperty<>(map)).createRegion(); } } 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 254e59d1..d8647d3a 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 @@ -22,9 +22,13 @@ import java.util.stream.Collectors; @JsonTypeName("script") public class SimpleScriptStore extends ScriptStore implements ScriptSnippet { + private final ShellDialect minimumDialect; + private final String commands; + private final ExecutionType executionType; + 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)) { @@ -42,10 +46,21 @@ public class SimpleScriptStore extends ScriptStore implements ScriptSnippet { } @Override - public List> getEffectiveScripts() { - return scripts != null - ? scripts.stream().filter(Objects::nonNull).toList() - : List.of(); + public String content(ShellControl shellControl) { + return assemble(shellControl, executionType); + } + + @Override + public ScriptSnippet.ExecutionType executionType() { + return executionType; + } + + @Override + public void checkComplete() throws Throwable { + Validators.nonNull(group); + super.checkComplete(); + Validators.nonNull(executionType); + Validators.nonNull(minimumDialect); } public void queryFlattenedScripts(LinkedHashSet all) { @@ -61,24 +76,7 @@ public class SimpleScriptStore extends ScriptStore implements ScriptSnippet { } @Override - public String content(ShellControl shellControl) { - return assemble(shellControl, executionType); - } - - @Override - public ScriptSnippet.ExecutionType executionType() { - return executionType; - } - - private final ShellDialect minimumDialect; - private final String commands; - private final ExecutionType executionType; - - @Override - public void checkComplete() throws Throwable { - Validators.nonNull(group); - super.checkComplete(); - Validators.nonNull(executionType); - Validators.nonNull(minimumDialect); + public List> getEffectiveScripts() { + return scripts != null ? scripts.stream().filter(Objects::nonNull).toList() : List.of(); } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java index f4d1c086..aae2856b 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java @@ -39,28 +39,19 @@ import java.util.stream.Collectors; public class SimpleScriptStoreProvider implements DataStoreProvider { - public String createInsightsMarkdown(DataStore store) { - var s = (SimpleScriptStore) store; - - var builder = MarkdownBuilder.of().addParagraph("XPipe will run the script in ") - .addCode(s.getMinimumDialect() != null ? s.getMinimumDialect().getDisplayName() : "default").add(" shells"); - - if (s.getEffectiveScripts() != null && !s.getEffectiveScripts().isEmpty()) { - builder.add(" with the following scripts prior").addCodeBlock(s.getEffectiveScripts().stream() - .map(scriptStoreDataStoreEntryRef -> scriptStoreDataStoreEntryRef.get().getName()).collect( - Collectors.joining("\n"))); - } - - if (s.getCommands() != null) { - builder.add(" with command contents").addCodeBlock(s.getCommands()); - } - - return builder.build(); + @Override + public boolean editByDefault() { + return true; } @Override - public Comp stateDisplay(StoreEntryWrapper w) { - return new SystemStateComp(new SimpleObjectProperty<>(SystemStateComp.State.SUCCESS)); + public boolean canMoveCategories() { + return false; + } + + @Override + public boolean shouldEdit() { + return true; } @Override @@ -106,14 +97,31 @@ public class SimpleScriptStoreProvider implements DataStoreProvider { } @Override - public boolean shouldEdit() { - return true; + public Comp stateDisplay(StoreEntryWrapper w) { + return new SystemStateComp(new SimpleObjectProperty<>(SystemStateComp.State.SUCCESS)); } - @Override - public DataStoreEntry getDisplayParent(DataStoreEntry store) { - SimpleScriptStore st = store.getStore().asNeeded(); - return st.getGroup().get(); + public String createInsightsMarkdown(DataStore store) { + var s = (SimpleScriptStore) store; + + var builder = MarkdownBuilder.of() + .addParagraph("XPipe will run the script in ") + .addCode(s.getMinimumDialect() != null ? s.getMinimumDialect().getDisplayName() : "default") + .add(" shells"); + + if (s.getEffectiveScripts() != null && !s.getEffectiveScripts().isEmpty()) { + builder.add(" with the following scripts prior") + .addCodeBlock(s.getEffectiveScripts().stream() + .map(scriptStoreDataStoreEntryRef -> + scriptStoreDataStoreEntryRef.get().getName()) + .collect(Collectors.joining("\n"))); + } + + if (s.getCommands() != null) { + builder.add(" with command contents").addCodeBlock(s.getCommands()); + } + + return builder.build(); } @Override @@ -122,26 +130,9 @@ public class SimpleScriptStoreProvider implements DataStoreProvider { } @Override - public String getId() { - return "script"; - } - - @SneakyThrows - @Override - public String getDisplayIconFileName(DataStore store) { - if (store == null) { - return "proc:shellEnvironment_icon.svg"; - } - - SimpleScriptStore st = store.asNeeded(); - return (String) Class.forName( - AppExtensionManager.getInstance() - .getExtendedLayer() - .findModule("io.xpipe.ext.proc") - .orElseThrow(), - "io.xpipe.ext.proc.ShellDialectChoiceComp") - .getDeclaredMethod("getImageName", ShellDialect.class) - .invoke(null, st.getMinimumDialect()); + public DataStoreEntry getDisplayParent(DataStoreEntry store) { + SimpleScriptStore st = store.getStore().asNeeded(); + return st.getGroup().get(); } @SneakyThrows @@ -172,8 +163,8 @@ public class SimpleScriptStoreProvider implements DataStoreProvider { new DataStoreListChoiceComp<>( others, ScriptStore.class, - scriptStore -> !scriptStore.get().equals(entry) && !others.contains(scriptStore), StoreViewState.get().getAllScriptsCategory() - ), + scriptStore -> !scriptStore.get().equals(entry) && !others.contains(scriptStore), + StoreViewState.get().getAllScriptsCategory()), others) .name("minimumShellDialect") .description("minimumShellDialectDescription") @@ -198,7 +189,12 @@ public class SimpleScriptStoreProvider implements DataStoreProvider { .description("scriptGroupDescription") .addComp( new DataStoreChoiceComp<>( - DataStoreChoiceComp.Mode.OTHER, null, group, ScriptGroupStore.class, null, StoreViewState.get().getAllScriptsCategory()), + DataStoreChoiceComp.Mode.OTHER, + null, + group, + ScriptGroupStore.class, + null, + StoreViewState.get().getAllScriptsCategory()), group) .nonNull() .bind( @@ -217,31 +213,7 @@ public class SimpleScriptStoreProvider implements DataStoreProvider { } @Override - public boolean editByDefault() { - return true; - } - - @Override - public boolean canMoveCategories() { - return false; - } - - @Override - public ObservableValue informationString(StoreEntryWrapper wrapper) { - SimpleScriptStore scriptStore = wrapper.getEntry().getStore().asNeeded(); - return new SimpleStringProperty((scriptStore.getMinimumDialect() != null - ? scriptStore.getMinimumDialect().getDisplayName() + " " - : "") - + (scriptStore.getExecutionType() == SimpleScriptStore.ExecutionType.TERMINAL_ONLY - ? "Terminal" - : scriptStore.getExecutionType() == SimpleScriptStore.ExecutionType.DUMB_ONLY - ? "Background" - : "") - + " Snippet"); - } - - @Override - public void storageInit() throws Exception { + public void storageInit() { DataStorage.get() .addStoreEntryIfNotPresent(DataStoreEntry.createNew( UUID.fromString("a9945ad2-db61-4304-97d7-5dc4330691a7"), @@ -280,8 +252,35 @@ public class SimpleScriptStoreProvider implements DataStoreProvider { } @Override - public List> getStoreClasses() { - return List.of(SimpleScriptStore.class); + public ObservableValue informationString(StoreEntryWrapper wrapper) { + SimpleScriptStore scriptStore = wrapper.getEntry().getStore().asNeeded(); + return new SimpleStringProperty((scriptStore.getMinimumDialect() != null + ? scriptStore.getMinimumDialect().getDisplayName() + " " + : "") + + (scriptStore.getExecutionType() == SimpleScriptStore.ExecutionType.TERMINAL_ONLY + ? "Terminal" + : scriptStore.getExecutionType() == SimpleScriptStore.ExecutionType.DUMB_ONLY + ? "Background" + : "") + + " Snippet"); + } + + @SneakyThrows + @Override + public String getDisplayIconFileName(DataStore store) { + if (store == null) { + return "proc:shellEnvironment_icon.svg"; + } + + SimpleScriptStore st = store.asNeeded(); + return (String) Class.forName( + AppExtensionManager.getInstance() + .getExtendedLayer() + .findModule("io.xpipe.ext.proc") + .orElseThrow(), + "io.xpipe.ext.proc.ShellDialectChoiceComp") + .getDeclaredMethod("getImageName", ShellDialect.class) + .invoke(null, st.getMinimumDialect()); } @Override @@ -296,4 +295,14 @@ public class SimpleScriptStoreProvider implements DataStoreProvider { public List getPossibleNames() { return Identifiers.get("script"); } + + @Override + public String getId() { + return "script"; + } + + @Override + public List> getStoreClasses() { + return List.of(SimpleScriptStore.class); + } } 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 00000000..8bb3a9d7 --- /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 00000000..219d112f --- /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 00000000..59cdf59b --- /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 00000000..cf924cbd --- /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 { + + @Override + public DataStoreCallSite getDataStoreCallSite() { + return new DataStoreCallSite() { + + @Override + public ActionProvider.Action createAction(DataStoreEntryRef store) { + return new Action(store); + } + + @Override + public Class getApplicableClass() { + return PauseableStore.class; + } + + @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"; + } + }; + } + + @Value + static class Action implements ActionProvider.Action { + + DataStoreEntryRef entry; + + @Override + public boolean requiresJavaFXPlatform() { + return false; + } + + @Override + public void execute() throws Exception { + entry.getStore().pause(); + } + } +} 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 00000000..90121ef5 --- /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 { + + @Override + public DataStoreCallSite getDataStoreCallSite() { + return new DataStoreCallSite() { + + @Override + public ActionProvider.Action createAction(DataStoreEntryRef store) { + return new Action(store); + } + + @Override + public Class getApplicableClass() { + return StartableStore.class; + } + + @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"; + } + }; + } + + @Value + static class Action implements ActionProvider.Action { + + DataStoreEntryRef entry; + + @Override + public boolean requiresJavaFXPlatform() { + return false; + } + + @Override + public void execute() throws Exception { + entry.getStore().start(); + } + } +} 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 00000000..36252510 --- /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 { + + @Override + public DataStoreCallSite getDataStoreCallSite() { + return new DataStoreCallSite() { + + @Override + public ActionProvider.Action createAction(DataStoreEntryRef store) { + return new Action(store); + } + + @Override + public Class getApplicableClass() { + return StoppableStore.class; + } + + @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"; + } + }; + } + + @Value + static class Action implements ActionProvider.Action { + + DataStoreEntryRef entry; + + @Override + public boolean requiresJavaFXPlatform() { + return false; + } + + @Override + public void execute() throws Exception { + entry.getStore().stop(); + } + } +} diff --git a/ext/base/src/main/java/module-info.java b/ext/base/src/main/java/module-info.java index 8007ac0e..0b6b8209 100644 --- a/ext/base/src/main/java/module-info.java +++ b/ext/base/src/main/java/module-info.java @@ -1,16 +1,19 @@ import io.xpipe.app.browser.action.BrowserAction; import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.ext.DataStoreProvider; -import io.xpipe.ext.base.InMemoryStoreProvider; 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 +24,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 @@ -46,10 +47,14 @@ open module io.xpipe.ext.base { NewItemAction, RenameAction, DeleteAction, + DeleteLinkAction, UnzipAction, JavapAction, JarAction; provides ActionProvider with + StoreStopAction, + StoreStartAction, + StorePauseAction, CloneStoreAction, RefreshStoreAction, ScanAction, @@ -60,6 +65,5 @@ open module io.xpipe.ext.base { BrowseStoreAction; provides DataStoreProvider with SimpleScriptStoreProvider, - ScriptGroupStoreProvider, - InMemoryStoreProvider; + ScriptGroupStoreProvider; } diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/scriptDependencies_en.md b/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/scriptDependencies_en.md index 6866d673..5acf5e51 100644 --- a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/scriptDependencies_en.md +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/scriptDependencies_en.md @@ -2,4 +2,4 @@ The scripts and script groups to run first. If an entire group is made a dependency, all scripts in this group will be considered as dependencies. -The resolved dependency graph of scripts is flattened, filtered, and made unique, i.e. only compatible scripts will be run and if script would be executed multiple times, it will only be run the first time. +The resolved dependency graph of scripts is flattened, filtered, and made unique. I.e. only compatible scripts will be run and if a script would be executed multiple times, it will only be run the first time. 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 c7c2b956..acac0433 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/main/resources/io/xpipe/ext/base/resources/scripts/clink.bat b/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/clink.bat index 33add306..55d31098 100644 --- a/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/clink.bat +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/clink.bat @@ -3,7 +3,7 @@ IF %ERRORLEVEL%==0 ( exit /b 0 ) -SET "PATH=%PATH%;%USERPROFILE%\.xpipe\scriptdata\clink" +SET "PATH=%PATH%;%TEMP%\xpipe\scriptdata\clink" WHERE clink >NUL 2>NUL IF %ERRORLEVEL%==0 ( exit /b 0 @@ -16,4 +16,4 @@ if ($defaultCreds) {^ $downloader.Credentials = $defaultCreds^ }^ $downloader.DownloadFile("https://github.com/chrisant996/clink/releases/download/v1.5.13/clink.1.5.13.290610.zip", "$env:TEMP\clink.zip");^ -Expand-Archive -Force -LiteralPath "$env:TEMP\clink.zip" -DestinationPath "$env:USERPROFILE\.xpipe\scriptdata\clink"; | powershell -NoLogo >NUL +Expand-Archive -Force -LiteralPath "$env:TEMP\clink.zip" -DestinationPath "$env:TEMP\xpipe\scriptdata\clink"; | powershell -NoLogo >NUL diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/starship_bash.sh b/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/starship_bash.sh index a532316f..190ceeda 100644 --- a/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/starship_bash.sh +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/starship_bash.sh @@ -1,4 +1,4 @@ -dir=~/.xpipe/scriptdata/starship +dir="/tmp/xpipe/$USER/scriptdata/starship" export PATH="$PATH:$dir" which starship > /dev/null 2>&1 if [ "$?" != 0 ]; then diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/starship_cmd.bat b/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/starship_cmd.bat index 3d7b6019..a3cfd5ba 100644 --- a/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/starship_cmd.bat +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/starship_cmd.bat @@ -4,6 +4,6 @@ IF NOT %ERRORLEVEL%==0 ( SET "PATH=%PATH%;C:\\Program Files\\starship\\bin" ) -MKDIR "%USERPROFILE%\\.xpipe\\scriptdata\\starship" >NUL 2>NUL -echo load(io.popen('starship init cmd'):read("*a"))() > "%USERPROFILE%\\.xpipe\\scriptdata\\starship\\starship.lua" -clink inject --quiet --profile "%USERPROFILE%\\.xpipe\\scriptdata\\starship" \ No newline at end of file +MKDIR "%TEMP%\\xpipe\\scriptdata\\starship" >NUL 2>NUL +echo load(io.popen('starship init cmd'):read("*a"))() > "%TEMP%\\xpipe\\scriptdata\\starship\\starship.lua" +clink inject --quiet --profile "%TEMP%\\xpipe\\scriptdata\\starship" \ No newline at end of file diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/starship_fish.fish b/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/starship_fish.fish index 20197928..d6613cb6 100644 --- a/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/starship_fish.fish +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/starship_fish.fish @@ -1,4 +1,4 @@ -set dir ~/.xpipe/scriptdata/starship +set dir "/tmp/xpipe/$USER/scriptdata/starship" export PATH="$PATH:$dir" which starship > /dev/null 2>&1 if [ $status != 0 ] diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/starship_zsh.sh b/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/starship_zsh.sh index f69997eb..c4dd1488 100644 --- a/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/starship_zsh.sh +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/starship_zsh.sh @@ -1,4 +1,4 @@ -dir=~/.xpipe/scriptdata/starship +dir="/tmp/xpipe/$USER/scriptdata/starship" export PATH="$PATH:$dir" which starship > /dev/null 2>&1 if [ "$?" != 0 ]; then diff --git a/ext/jdbc/build.gradle b/ext/jdbc/build.gradle index ff4aadf5..3e834dfa 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 ff4aadf5..3e834dfa 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 ff4aadf5..3e834dfa 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.ps1 b/get-xpipe.ps1 index 3f6529c2..dac17fc9 100644 --- a/get-xpipe.ps1 +++ b/get-xpipe.ps1 @@ -146,8 +146,7 @@ if (-not $env:TEMP) { $env:TEMP = Join-Path $env:SystemDrive -ChildPath 'temp' } -$xpipeTempDir = Join-Path $env:TEMP -ChildPath "xpipe" -$tempDir = Join-Path $xpipeTempDir -ChildPath "install" +$tempDir = $env:TEMP if (-not (Test-Path $tempDir -PathType Container)) { $null = New-Item -Path $tempDir -ItemType Directory @@ -178,7 +177,7 @@ $env:Path=( ) -match '.' -join ';' Write-Host -Write-Host 'XPipe has been successfully installed. You should be able to find it in your applications.' +Write-Host "$ProductName has been successfully installed. You should be able to find it in your applications." Write-Host # Use absolute path as we can't assume that the user has selected to put XPipe into the Path diff --git a/get-xpipe.sh b/get-xpipe.sh index c25a8c3d..9a7ad4f1 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 0b7e133a..19032f61 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/extension.gradle b/gradle/gradle_scripts/extension.gradle index 706c79db..7975d1d8 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/local_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/javafx.gradle b/gradle/gradle_scripts/javafx.gradle index d302d9e9..47bc81ae 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 608132b3..0d96ea7a 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/local_junit_suite.gradle b/gradle/gradle_scripts/local_junit_suite.gradle new file mode 100644 index 00000000..25dba422 --- /dev/null +++ b/gradle/gradle_scripts/local_junit_suite.gradle @@ -0,0 +1,37 @@ +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" + } + } + } + } + } +} diff --git a/gradle/gradle_scripts/lombok.gradle b/gradle/gradle_scripts/lombok.gradle index cc8da78b..f132bbf0 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/remote_junit_suite.gradle b/gradle/gradle_scripts/remote_junit_suite.gradle new file mode 100644 index 00000000..6df6e012 --- /dev/null +++ b/gradle/gradle_scripts/remote_junit_suite.gradle @@ -0,0 +1,46 @@ +apply from: "$rootDir/gradle/gradle_scripts/junit.gradle" + +testing { + suites { + remoteTest(JvmTestSuite) { + useJUnitJupiter() + + dependencies { + implementation project(':core') + implementation project(':beacon') + 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/modules.gradle b/modules.gradle new file mode 100644 index 00000000..20ca45bd --- /dev/null +++ b/modules.gradle @@ -0,0 +1,71 @@ +extraJavaModuleInfo { + module("markdowngenerator-1.3.1.1.jar", "net.steppschuh.markdowngenerator") { + exportAllPackages() + } +} + +extraJavaModuleInfo { + module("org.apache.commons:commons-lang3", "org.apache.commons.lang3") { + exportAllPackages() + } + 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/setup.sh b/setup.sh index da35d484..1844c1d8 100755 --- a/setup.sh +++ b/setup.sh @@ -10,5 +10,5 @@ if [ $? -ne 0 ]; then . "$HOME/.sdkman/bin/sdkman-init.sh" fi; -sdk install java 21-graalce -sdk default java 21-graalce +sdk install java 21.0.2-graalce +sdk default java 21.0.2-graalce diff --git a/version b/version index 33e7e629..58c43cba 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.7.16 \ No newline at end of file +8.0-11