diff --git a/.gitignore b/.gitignore index 48b701855..04312448a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ ComponentsGenerated.wxs !dist/javafx/**/lib !dist/javafx/**/bin dev.properties +xcuserdata/ +*.dylib +project.xcworkspace diff --git a/README.md b/README.md index accfd3480..15960dcda 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ It currently supports: - Quickly perform various commonly used actions like starting/stopping containers, establishing tunnels, and more - Create desktop shortcuts that automatically open remote connections in your terminal without having to open any GUI -![connections](https://github.com/xpipe-io/xpipe/assets/72509152/5df3169a-4150-4478-a3de-ae1f9748c3c8) +![connections](https://github.com/user-attachments/assets/07312929-1792-4589-b139-aa10bbcdc649) ## Powerful file management @@ -39,8 +39,8 @@ It currently supports: - Dynamically elevate sessions with sudo when required without having to restart the session - Seamlessly transfer files from and to your system desktop environment - Work and perform transfers on multiple systems at the same time with the built-in tabbed multitasking - -![browser](https://github.com/xpipe-io/xpipe/assets/72509152/4d4e4e54-17c1-4ebe-acf8-f615cfce8b3f) + +![browser](https://github.com/user-attachments/assets/7e5d8b3b-8cd7-4b71-ad79-9afb385de3fd) ## Terminal launcher @@ -52,7 +52,7 @@ It currently supports:

- Terminal launcher + Terminal launcher


@@ -63,7 +63,7 @@ It currently supports: - Setup shell init environments for connections to fully customize your work environment for every purpose - Open custom shells and custom remote connections by providing your own commands -![scripts](https://github.com/xpipe-io/xpipe/assets/72509152/56533f22-b689-4201-b58a-eebe0a6d517a) +![scripts](https://github.com/user-attachments/assets/cf39afaf-638d-48fc-9247-4c8d847d4ed4) ## Secure vault @@ -72,7 +72,7 @@ It currently supports: - There are no servers involved, all your information stays on your systems. The XPipe application does not send any personal or sensitive information to outside services. - Vault changes can be pushed and pulled from your own remote git repository by multiple team members across many systems -## API +## Programmatic connection control via the API - The XPipe API provides programmatic access to XPipe’s features via an HTTP interface - Manage all your remote systems and access their file systems in your own favorite programming language @@ -197,6 +197,7 @@ The distributed XPipe application consists out of two parts: - The closed-source extensions, mostly for professional edition features, which are not included in this repository Additional features are available in the professional edition. For more details see https://xpipe.io/pricing. +If your enterprise puts great emphasis on having access to the full source code, there are also full source-available enterprise options available. ## More links diff --git a/app/build.gradle b/app/build.gradle index 613c5fe18..a3cf49751 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ dependencies { api project(':beacon') 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' + compileOnly 'org.junit.jupiter:junit-jupiter-api:5.10.3' + compileOnly 'org.junit.jupiter:junit-jupiter-params:5.10.3' api 'com.vladsch.flexmark:flexmark:0.64.8' api 'com.vladsch.flexmark:flexmark-util:0.64.8' @@ -50,22 +50,23 @@ dependencies { api files("$rootDir/gradle/gradle_scripts/markdowngenerator-1.3.1.1.jar") api files("$rootDir/gradle/gradle_scripts/vernacular-1.16.jar") + api 'org.bouncycastle:bcprov-jdk18on:1.78.1' api 'info.picocli:picocli:4.7.6' - api ('org.kohsuke:github-api:1.322') { + api ('org.kohsuke:github-api:1.323') { exclude group: 'org.apache.commons', module: 'commons-lang3' } - api 'org.apache.commons:commons-lang3:3.14.0' - api 'io.sentry:sentry:7.10.0' + api 'org.apache.commons:commons-lang3:3.16.0' + api 'io.sentry:sentry:7.13.0' api 'commons-io:commons-io:2.16.1' - api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.17.1" - api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.17.1" + api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.17.2" + api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.17.2" api group: 'org.kordamp.ikonli', name: 'ikonli-material2-pack', version: "12.2.0" api group: 'org.kordamp.ikonli', name: 'ikonli-materialdesign2-pack', version: "12.2.0" api group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0" api group: 'org.kordamp.ikonli', name: 'ikonli-material-pack', version: "12.2.0" api group: 'org.kordamp.ikonli', name: 'ikonli-feather-pack', version: "12.2.0" - api group: 'org.slf4j', name: 'slf4j-api', version: '2.0.13' - api group: 'org.slf4j', name: 'slf4j-jdk-platform-logging', version: '2.0.13' + api group: 'org.slf4j', name: 'slf4j-api', version: '2.0.15' + api group: 'org.slf4j', name: 'slf4j-jdk-platform-logging', version: '2.0.15' api 'io.xpipe:modulefs:0.1.5' api 'net.synedra:validatorfx:0.4.2' api files("$rootDir/gradle/gradle_scripts/atlantafx-base-2.0.2.jar") @@ -93,6 +94,7 @@ run { systemProperty 'io.xpipe.app.logLevel', "trace" systemProperty 'io.xpipe.app.fullVersion', rootProject.fullVersion systemProperty 'io.xpipe.app.staging', isStage + // systemProperty 'io.xpipe.beacon.port', "30000" // Apply passed xpipe properties for (final def e in System.getProperties().entrySet()) { diff --git a/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java b/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java index 93af376e5..be4e4d8cd 100644 --- a/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java +++ b/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java @@ -1,22 +1,23 @@ package io.xpipe.app.beacon; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; import io.xpipe.app.core.AppResources; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.util.MarkdownHelper; import io.xpipe.beacon.BeaconConfig; import io.xpipe.beacon.BeaconInterface; +import io.xpipe.core.process.OsType; import io.xpipe.core.util.XPipeInstallation; - -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpServer; import lombok.Getter; import java.io.IOException; -import java.net.InetAddress; +import java.net.Inet4Address; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.attribute.PosixFilePermissions; import java.util.*; import java.util.concurrent.Executors; import java.util.regex.Pattern; @@ -84,6 +85,7 @@ public class AppBeaconServer { public static void reset() { if (INSTANCE != null) { INSTANCE.stop(); + INSTANCE.deleteAuthSecret(); INSTANCE = null; } } @@ -109,11 +111,22 @@ public class AppBeaconServer { var file = XPipeInstallation.getLocalBeaconAuthFile(); var id = UUID.randomUUID().toString(); Files.writeString(file, id); + if (OsType.getLocal() != OsType.WINDOWS) { + Files.setPosixFilePermissions(file, PosixFilePermissions.fromString("rw-rw----")); + } localAuthSecret = id; } + private void deleteAuthSecret() { + var file = XPipeInstallation.getLocalBeaconAuthFile(); + try { + Files.delete(file); + } catch (IOException ignored) { + } + } + private void start() throws IOException { - server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), port), 10); + server = HttpServer.create(new InetSocketAddress(Inet4Address.getByAddress(new byte[]{ 0x7f,0x00,0x00,0x01 }), port), 10); BeaconInterface.getAll().forEach(beaconInterface -> { server.createContext(beaconInterface.getPath(), new BeaconRequestHandler<>(beaconInterface)); }); diff --git a/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java b/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java index d231c96a0..019b70096 100644 --- a/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java @@ -28,7 +28,8 @@ public class BeaconRequestHandler implements HttpHandler { @Override public void handle(HttpExchange exchange) { - if (OperationMode.isInShutdown()) { + if (OperationMode.isInShutdown() && !beaconInterface.acceptInShutdown()) { + writeError(exchange, new BeaconClientErrorResponse("Daemon is currently in shutdown"), 400); return; } @@ -108,7 +109,7 @@ public class BeaconRequestHandler implements HttpHandler { // Make deserialization error message more readable var message = ex.getMessage() .replace("$RequestBuilder", "") - .replace("Exchange$Request","Request") + .replace("Exchange$Request", "Request") .replace("at [Source: UNKNOWN; byte offset: #UNKNOWN]", "") .replaceAll("(\\w+) is marked non-null but is null", "field $1 is missing from object") .trim(); @@ -124,10 +125,13 @@ public class BeaconRequestHandler implements HttpHandler { try { var emptyResponseClass = beaconInterface.getResponseClass().getDeclaredFields().length == 0; if (!emptyResponseClass && response != null) { - TrackEvent.trace("Sending response:\n" + object); - var tree = JacksonMapper.getDefault().valueToTree(response); - TrackEvent.trace("Sending raw response:\n" + tree.toPrettyString()); - var bytes = tree.toPrettyString().getBytes(StandardCharsets.UTF_8); + TrackEvent.trace("Sending response:\n" + response); + TrackEvent.trace("Sending raw response:\n" + + JacksonMapper.getCensored().valueToTree(response).toPrettyString()); + var bytes = JacksonMapper.getDefault() + .valueToTree(response) + .toPrettyString() + .getBytes(StandardCharsets.UTF_8); exchange.sendResponseHeaders(200, bytes.length); try (OutputStream os = exchange.getResponseBody()) { os.write(bytes); diff --git a/app/src/main/java/io/xpipe/app/beacon/BlobManager.java b/app/src/main/java/io/xpipe/app/beacon/BlobManager.java index 466ce056d..fc12a1b42 100644 --- a/app/src/main/java/io/xpipe/app/beacon/BlobManager.java +++ b/app/src/main/java/io/xpipe/app/beacon/BlobManager.java @@ -50,7 +50,7 @@ public class BlobManager { public Path newBlobFile() throws IOException { var file = TEMP.resolve(UUID.randomUUID().toString()); - Files.createDirectories(file.getParent()); + FileUtils.forceMkdir(file.getParent().toFile()); return file; } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java index e5704ecad..7cf0c747f 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionAddExchangeImpl.java @@ -1,12 +1,13 @@ package io.xpipe.app.beacon.impl; -import com.sun.net.httpserver.HttpExchange; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.beacon.api.ConnectionAddExchange; import io.xpipe.core.util.ValidationException; +import com.sun.net.httpserver.HttpExchange; + public class ConnectionAddExchangeImpl extends ConnectionAddExchange { @Override diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionBrowseExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionBrowseExchangeImpl.java index 250e65eec..1fb2f1d2a 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionBrowseExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionBrowseExchangeImpl.java @@ -1,6 +1,5 @@ package io.xpipe.app.beacon.impl; -import com.sun.net.httpserver.HttpExchange; import io.xpipe.app.browser.session.BrowserSessionModel; import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.storage.DataStorage; @@ -8,6 +7,8 @@ import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.api.ConnectionBrowseExchange; import io.xpipe.core.store.FileSystemStore; +import com.sun.net.httpserver.HttpExchange; + public class ConnectionBrowseExchangeImpl extends ConnectionBrowseExchange { @Override @@ -18,7 +19,8 @@ public class ConnectionBrowseExchangeImpl extends ConnectionBrowseExchange { if (!(e.getStore() instanceof FileSystemStore)) { throw new BeaconClientException("Not a file system connection"); } - BrowserSessionModel.DEFAULT.openFileSystemSync(e.ref(),msg.getDirectory() != null ? ignored -> msg.getDirectory() : null,null); + BrowserSessionModel.DEFAULT.openFileSystemSync( + e.ref(), msg.getDirectory() != null ? ignored -> msg.getDirectory() : null, null); AppLayoutModel.get().selectBrowser(); return Response.builder().build(); } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionInfoExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionInfoExchangeImpl.java index 2efaf15b3..a4c454b63 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionInfoExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionInfoExchangeImpl.java @@ -28,9 +28,16 @@ public class ConnectionInfoExchangeImpl extends ConnectionInfoExchange { .orElseThrow()) .getNames(); var cat = new StorePath(names.subList(1, names.size())); - var cache = e.getStoreCache().entrySet().stream().filter(stringObjectEntry -> { - return stringObjectEntry.getValue() != null && (ClassUtils.isPrimitiveOrWrapper(stringObjectEntry.getValue().getClass()) || stringObjectEntry.getValue() instanceof String); - }).collect(Collectors.toMap(stringObjectEntry -> stringObjectEntry.getKey(),stringObjectEntry -> stringObjectEntry.getValue())); + var cache = e.getStoreCache().entrySet().stream() + .filter(stringObjectEntry -> { + return stringObjectEntry.getValue() != null + && (ClassUtils.isPrimitiveOrWrapper( + stringObjectEntry.getValue().getClass()) + || stringObjectEntry.getValue() instanceof String); + }) + .collect(Collectors.toMap( + stringObjectEntry -> stringObjectEntry.getKey(), + stringObjectEntry -> stringObjectEntry.getValue())); var apply = InfoResponse.builder() .lastModified(e.getLastModified()) @@ -50,27 +57,17 @@ public class ConnectionInfoExchangeImpl extends ConnectionInfoExchange { } private Class toWrapper(Class clazz) { - if (!clazz.isPrimitive()) - return clazz; + if (!clazz.isPrimitive()) return clazz; - if (clazz == Integer.TYPE) - return Integer.class; - if (clazz == Long.TYPE) - return Long.class; - if (clazz == Boolean.TYPE) - return Boolean.class; - if (clazz == Byte.TYPE) - return Byte.class; - if (clazz == Character.TYPE) - return Character.class; - if (clazz == Float.TYPE) - return Float.class; - if (clazz == Double.TYPE) - return Double.class; - if (clazz == Short.TYPE) - return Short.class; - if (clazz == Void.TYPE) - return Void.class; + if (clazz == Integer.TYPE) return Integer.class; + if (clazz == Long.TYPE) return Long.class; + if (clazz == Boolean.TYPE) return Boolean.class; + if (clazz == Byte.TYPE) return Byte.class; + if (clazz == Character.TYPE) return Character.class; + if (clazz == Float.TYPE) return Float.class; + if (clazz == Double.TYPE) return Double.class; + if (clazz == Short.TYPE) return Short.class; + if (clazz == Void.TYPE) return Void.class; return clazz; } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRefreshExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRefreshExchangeImpl.java index dfd656d96..5fa336528 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRefreshExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRefreshExchangeImpl.java @@ -1,11 +1,12 @@ package io.xpipe.app.beacon.impl; -import com.sun.net.httpserver.HttpExchange; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.util.FixedHierarchyStore; import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.api.ConnectionRefreshExchange; +import com.sun.net.httpserver.HttpExchange; + public class ConnectionRefreshExchangeImpl extends ConnectionRefreshExchange { @Override diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRemoveExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRemoveExchangeImpl.java index 88f3850cb..ed7c65907 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRemoveExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionRemoveExchangeImpl.java @@ -1,11 +1,12 @@ package io.xpipe.app.beacon.impl; -import com.sun.net.httpserver.HttpExchange; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.api.ConnectionRemoveExchange; +import com.sun.net.httpserver.HttpExchange; + import java.util.ArrayList; import java.util.UUID; diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionTerminalExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionTerminalExchangeImpl.java index 799be53b1..0717de7f4 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionTerminalExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionTerminalExchangeImpl.java @@ -1,12 +1,13 @@ package io.xpipe.app.beacon.impl; -import com.sun.net.httpserver.HttpExchange; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.util.TerminalLauncher; import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.api.ConnectionTerminalExchange; import io.xpipe.core.store.ShellStore; +import com.sun.net.httpserver.HttpExchange; + public class ConnectionTerminalExchangeImpl extends ConnectionTerminalExchange { @Override @@ -18,7 +19,7 @@ public class ConnectionTerminalExchangeImpl extends ConnectionTerminalExchange { throw new BeaconClientException("Not a shell connection"); } try (var sc = shellStore.control().start()) { - TerminalLauncher.open(e,e.getName(),msg.getDirectory(),sc); + TerminalLauncher.open(e, e.getName(), msg.getDirectory(), sc); } return Response.builder().build(); } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionToggleExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionToggleExchangeImpl.java index dbecdba4d..7d3f48dd1 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionToggleExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionToggleExchangeImpl.java @@ -1,11 +1,12 @@ package io.xpipe.app.beacon.impl; -import com.sun.net.httpserver.HttpExchange; import io.xpipe.app.storage.DataStorage; import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.api.ConnectionToggleExchange; import io.xpipe.core.store.SingletonSessionStore; +import com.sun.net.httpserver.HttpExchange; + public class ConnectionToggleExchangeImpl extends ConnectionToggleExchange { @Override diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ShellStartExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ShellStartExchangeImpl.java index 9d05484a6..9ff60fffe 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/ShellStartExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ShellStartExchangeImpl.java @@ -36,6 +36,7 @@ public class ShellStartExchangeImpl extends ShellStartExchange { .osType(control.getOsType()) .osName(control.getOsName()) .temp(control.getSystemTemporaryDirectory()) + .ttyState(control.getTtyState()) .build(); } } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkComp.java index 2a18d4643..446438b16 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkComp.java @@ -10,10 +10,12 @@ import io.xpipe.app.storage.DataStoreEntry; import javafx.beans.binding.Bindings; import javafx.beans.property.*; import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; import javafx.css.PseudoClass; import javafx.scene.control.Button; import javafx.scene.layout.Region; +import java.util.HashSet; import java.util.function.BiConsumer; import java.util.function.Predicate; @@ -41,13 +43,13 @@ public final class BrowserBookmarkComp extends SimpleComp { @Override protected Region createSimple() { - BooleanProperty busy = new SimpleBooleanProperty(false); + var busyEntries = FXCollections.observableSet(new HashSet<>()); BiConsumer>> augment = (s, comp) -> { comp.disable(Bindings.createBooleanBinding( () -> { - return busy.get() || !applicable.test(s.getWrapper()); + return busyEntries.contains(s) || !applicable.test(s.getWrapper()); }, - busy)); + busyEntries)); comp.apply(struc -> { selected.addListener((observable, oldValue, newValue) -> { PlatformThread.runLaterIfNeeded(() -> { @@ -70,7 +72,17 @@ public final class BrowserBookmarkComp extends SimpleComp { category, StoreViewState.get().getEntriesListUpdateObservable()), augment, - entryWrapper -> action.accept(entryWrapper, busy)); + selectedAction -> { + BooleanProperty busy = new SimpleBooleanProperty(false); + action.accept(selectedAction.getWrapper(), busy); + busy.addListener((observable, oldValue, newValue) -> { + if (newValue) { + busyEntries.add(selectedAction); + } else { + busyEntries.remove(selectedAction); + } + }); + }); var r = section.vgrow().createRegion(); r.getStyleClass().add("bookmark-list"); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkHeaderComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkHeaderComp.java index 2c59091b1..6e67c0a84 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkHeaderComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkHeaderComp.java @@ -2,6 +2,7 @@ package io.xpipe.app.browser; import io.xpipe.app.comp.store.StoreCategoryWrapper; 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; @@ -30,18 +31,26 @@ public final class BrowserBookmarkHeaderComp extends SimpleComp { StoreViewState.get().getAllConnectionsCategory(), StoreViewState.get().getActiveCategory(), this.category) - .styleClass(Styles.LEFT_PILL); - var filter = new FilterComp(this.filter).styleClass(Styles.RIGHT_PILL).minWidth(0).hgrow(); + .styleClass(Styles.LEFT_PILL) + .apply(struc -> { + AppFont.medium(struc.get()); + }); + var filter = new FilterComp(this.filter) + .styleClass(Styles.RIGHT_PILL) + .minWidth(0) + .hgrow() + .apply(struc -> { + AppFont.medium(struc.get()); + }); var top = new HorizontalComp(List.of(category, filter)) .apply(struc -> struc.get().setFillHeight(true)) .apply(struc -> { - ((Region) struc.get().getChildren().get(0)) - .prefHeightProperty() - .bind(((Region) struc.get().getChildren().get(1)).heightProperty()); - ((Region) struc.get().getChildren().get(0)) - .minWidthProperty() - .bind(struc.get().widthProperty().divide(2.0)); + var first = ((Region) struc.get().getChildren().get(0)); + var second = ((Region) struc.get().getChildren().get(1)); + first.prefHeightProperty().bind(second.heightProperty()); + first.minHeightProperty().bind(second.heightProperty()); + first.maxHeightProperty().bind(second.heightProperty()); }) .styleClass("bookmarks-header") .createRegion(); 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 39290d0e0..f4c4f55f6 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java @@ -109,7 +109,7 @@ public class BrowserNavBar extends Comp { new TooltipAugment<>("history", new KeyCodeCombination(KeyCode.H, KeyCombination.ALT_DOWN)) .augment(historyButton); - var breadcrumbs = new BrowserBreadcrumbBar(model).grow(false, true); + var breadcrumbs = new BrowserBreadcrumbBar(model); var pathRegion = pathBar.createStructure().get(); var breadcrumbsRegion = breadcrumbs.createRegion(); @@ -143,7 +143,7 @@ public class BrowserNavBar extends Comp { topBox.setFillHeight(true); topBox.setAlignment(Pos.CENTER); homeButton.minWidthProperty().bind(pathRegion.heightProperty()); - homeButton.maxWidthProperty().bind(pathRegion.heightProperty().multiply(1.3)); + homeButton.maxWidthProperty().bind(pathRegion.heightProperty()); homeButton.minHeightProperty().bind(pathRegion.heightProperty()); homeButton.maxHeightProperty().bind(pathRegion.heightProperty()); historyButton.minHeightProperty().bind(pathRegion.heightProperty()); 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 82528ffd2..cf78e25d7 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserSavedStateImpl.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserSavedStateImpl.java @@ -29,7 +29,16 @@ public class BrowserSavedStateImpl implements BrowserSavedState { this.lastSystems = FXCollections.observableArrayList(lastSystems); } - public static BrowserSavedStateImpl load() { + private static BrowserSavedStateImpl INSTANCE; + + public static BrowserSavedState get() { + if (INSTANCE == null) { + INSTANCE = load(); + } + return INSTANCE; + } + + private static BrowserSavedStateImpl load() { return AppCache.get("browser-state", BrowserSavedStateImpl.class, () -> { return new BrowserSavedStateImpl(FXCollections.observableArrayList()); }); 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 93ae14cec..624d9f618 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserSelectionListComp.java @@ -7,6 +7,7 @@ import io.xpipe.app.core.window.AppWindowHelper; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.impl.PrettyImageHelper; +import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; import javafx.beans.binding.Bindings; @@ -58,9 +59,15 @@ public class BrowserSelectionListComp extends SimpleComp { return Comp.of(() -> { var image = PrettyImageHelper.ofFixedSizeSquare(entry.getIcon(), 24) .createRegion(); - var l = new Label(null, image); + var t = nameTransformation.apply(entry); + var l = new Label(t.getValue(), image); l.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); - l.textProperty().bind(PlatformThread.sync(nameTransformation.apply(entry))); + t.addListener((observable, oldValue, newValue) -> { + PlatformThread.runLaterIfNeeded(() -> { + l.setText(newValue); + }); + }); + BindingsHelper.preserve(l, t); return l; }); }, 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 f784fe6f3..8d853d5fd 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserStatusBarComp.java @@ -12,10 +12,12 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.LabelComp; import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.util.HumanReadableFormat; + import javafx.beans.binding.Bindings; import javafx.geometry.Pos; import javafx.scene.input.MouseButton; import javafx.scene.layout.Region; + import lombok.EqualsAndHashCode; import lombok.Value; @@ -37,8 +39,7 @@ public class BrowserStatusBarComp extends SimpleComp { createProgressEstimateStatus(), Comp.hspacer(), createClipboardStatus(), - createSelectionStatus() - )); + createSelectionStatus())); bar.spacing(15); bar.styleClass("status-bar"); @@ -58,12 +59,16 @@ public class BrowserStatusBarComp extends SimpleComp { return null; } else { var expected = p.expectedTimeRemaining(); - var show = (p.getTotal() > 50_000_000 && p.elapsedTime().compareTo(Duration.of(200, ChronoUnit.MILLIS)) > 0) || expected.toMillis() > 5000; - var time = show ? HumanReadableFormat.duration(p.expectedTimeRemaining()) : "..."; + var show = p.elapsedTime().compareTo(Duration.of(200, ChronoUnit.MILLIS)) > 0 + && (p.getTotal() > 50_000_000 || expected.toMillis() > 5000); + var time = show ? HumanReadableFormat.duration(p.expectedTimeRemaining()) : ""; return time; } }); - var progressComp = new LabelComp(text).styleClass("progress").apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)).prefWidth(90); + var progressComp = new LabelComp(text) + .styleClass("progress") + .apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)) + .prefWidth(90); return progressComp; } @@ -77,7 +82,10 @@ public class BrowserStatusBarComp extends SimpleComp { return transferred + " / " + all; } }); - var progressComp = new LabelComp(text).styleClass("progress").apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)).prefWidth(150); + var progressComp = new LabelComp(text) + .styleClass("progress") + .apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)) + .prefWidth(150); return progressComp; } @@ -89,7 +97,10 @@ public class BrowserStatusBarComp extends SimpleComp { return p.getName(); } }); - var progressComp = new LabelComp(text).styleClass("progress").apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)).prefWidth(180); + var progressComp = new LabelComp(text) + .styleClass("progress") + .apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)) + .prefWidth(180); return progressComp; } @@ -160,7 +171,6 @@ public class BrowserStatusBarComp extends SimpleComp { emptyEntry.onDragDone(event); }); - // Use status bar as an extension of file list new ContextMenuAugment<>( mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY, 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 e4bfb2a2d..5c3334684 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java @@ -1,8 +1,6 @@ package io.xpipe.app.browser; -import io.xpipe.app.browser.file.BrowserFileTransferMode; import io.xpipe.app.browser.fs.OpenFileSystemModel; -import io.xpipe.app.comp.base.LoadingOverlayComp; import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.Comp; @@ -10,15 +8,19 @@ import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.augment.DragOverPseudoClassAugment; import io.xpipe.app.fxcomps.impl.*; import io.xpipe.app.fxcomps.util.DerivedObservableList; -import io.xpipe.app.fxcomps.util.PlatformThread; +import io.xpipe.app.util.ThreadHelper; + import javafx.beans.binding.Bindings; +import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; import javafx.css.PseudoClass; 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.Region; + import org.kordamp.ikonli.javafx.FontIcon; import java.io.File; @@ -37,172 +39,151 @@ public class BrowserTransferComp extends SimpleComp { @Override protected Region createSimple() { - var syncItems = PlatformThread.sync(model.getItems()); - var syncDownloaded = PlatformThread.sync(model.getDownloading()); - var syncAllDownloaded = PlatformThread.sync(model.getAllDownloaded()); - var background = new LabelComp(AppI18n.observable("transferDescription")) .apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline"))) .apply(struc -> struc.get().setWrapText(true)) - .visible(Bindings.isEmpty(syncItems)); + .visible(model.getEmpty()); var backgroundStack = new StackComp(List.of(background)).grow(true, true).styleClass("download-background"); - var binding = new DerivedObservableList<>(syncItems, true) + var binding = new DerivedObservableList<>(model.getItems(), true) .mapped(item -> item.getBrowserEntry()) .getList(); - var list = new BrowserSelectionListComp( - binding, - entry -> Bindings.createStringBinding( + var list = new BrowserSelectionListComp(binding, entry -> { + var sourceItem = model.getCurrentItems().stream() + .filter(item -> item.getBrowserEntry() == entry) + .findAny(); + if (sourceItem.isEmpty()) { + return new SimpleStringProperty("?"); + } + synchronized (sourceItem.get().getProgress()) { + return Bindings.createStringBinding( () -> { - var sourceItem = syncItems.stream() - .filter(item -> item.getBrowserEntry() == entry) - .findAny(); - if (sourceItem.isEmpty()) { - return "?"; - } - var name = entry.getModel() == null + var p = sourceItem.get().getProgress().getValue(); + var progressSuffix = p == null || sourceItem .get() .downloadFinished() .get() - ? "Local" - : entry.getModel() - .getFileSystemModel() - .getName(); - return entry.getFileName() + " (" + name + ")"; + ? "" + : " " + (p.getTransferred() * 100 / p.getTotal()) + "%"; + return entry.getFileName() + progressSuffix; }, - syncAllDownloaded)) + sourceItem.get().getProgress()); + } + }) .grow(false, true); - var dragNotice = new LabelComp(syncAllDownloaded.flatMap( - aBoolean -> aBoolean ? AppI18n.observable("dragLocalFiles") : AppI18n.observable("dragFiles"))) + var dragNotice = new LabelComp(AppI18n.observable("dragLocalFiles")) .apply(struc -> struc.get().setGraphic(new FontIcon("mdi2h-hand-left"))) .apply(struc -> AppFont.medium(struc.get())) .apply(struc -> struc.get().setWrapText(true)) - .hide(Bindings.isEmpty(syncItems)); + .hide(model.getEmpty()); - var downloadButton = new IconButtonComp("mdi2d-download", () -> { - model.download(); - }) - .hide(Bindings.isEmpty(syncItems)) - .disable(syncAllDownloaded) - .tooltipKey("downloadStageDescription"); var clearButton = new IconButtonComp("mdi2c-close", () -> { - model.clear(true); + ThreadHelper.runAsync(() -> { + model.clear(true); + }); }) - .hide(Bindings.isEmpty(syncItems)) + .hide(model.getEmpty()) .tooltipKey("clearTransferDescription"); - var bottom = - new HorizontalComp(List.of(Comp.hspacer(), dragNotice, Comp.hspacer(), downloadButton, Comp.hspacer(4), clearButton)); + var downloadButton = new IconButtonComp("mdi2f-folder-move-outline", () -> { + ThreadHelper.runFailableAsync(() -> { + model.transferToDownloads(); + }); + }) + .hide(model.getEmpty()) + .tooltipKey("downloadStageDescription"); + + var bottom = new HorizontalComp( + List.of(Comp.hspacer(), dragNotice, Comp.hspacer(), downloadButton, Comp.hspacer(4), clearButton)); var listBox = new VerticalComp(List.of(list, bottom)) .spacing(5) .padding(new Insets(10, 10, 5, 10)) .apply(struc -> struc.get().setMinHeight(200)) .apply(struc -> struc.get().setMaxHeight(200)); - var stack = LoadingOverlayComp.noProgress( - new StackComp(List.of(backgroundStack, listBox)) - .apply(DragOverPseudoClassAugment.create()) - .apply(struc -> { - struc.get().setOnDragOver(event -> { - // Accept drops from inside the app window - if (event.getGestureSource() != null && event.getGestureSource() != struc.get()) { - event.acceptTransferModes(TransferMode.ANY); - event.consume(); - } + var stack = new StackComp(List.of(backgroundStack, listBox)) + .apply(DragOverPseudoClassAugment.create()) + .apply(struc -> { + struc.get().setOnDragOver(event -> { + // Accept drops from inside the app window + if (event.getGestureSource() != null && event.getGestureSource() != struc.get()) { + event.acceptTransferModes(TransferMode.ANY); + event.consume(); + } + }); + struc.get().setOnDragDropped(event -> { + // Accept drops from inside the app window + if (event.getGestureSource() != null) { + var drag = BrowserClipboard.retrieveDrag(event.getDragboard()); + if (drag == null) { + return; + } - // 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 drag = BrowserClipboard.retrieveDrag(event.getDragboard()); - if (drag == null) { - return; + if (!(model.getBrowserSessionModel() + .getSelectedEntry() + .getValue() + instanceof OpenFileSystemModel fileSystemModel)) { + return; + } + + var files = drag.getEntries(); + model.drop(fileSystemModel, files); + event.setDropCompleted(true); + event.consume(); + } + }); + struc.get().setOnDragDetected(event -> { + var items = model.getCurrentItems(); + var selected = items.stream() + .map(item -> item.getBrowserEntry()) + .toList(); + var files = items.stream() + .filter(item -> item.downloadFinished().get()) + .map(item -> { + try { + var file = item.getLocalFile(); + if (!Files.exists(file)) { + return Optional.empty(); + } + + return Optional.of(file.toRealPath().toFile()); + } catch (IOException e) { + throw new RuntimeException(e); } + }) + .flatMap(Optional::stream) + .toList(); + if (files.isEmpty()) { + return; + } - if (!(model.getBrowserSessionModel() - .getSelectedEntry() - .getValue() - instanceof OpenFileSystemModel fileSystemModel)) { - return; - } + var cc = new ClipboardContent(); + cc.putFiles(files); + Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY); + db.setContent(cc); - var files = drag.getEntries(); - model.drop(fileSystemModel, files); - event.setDropCompleted(true); - event.consume(); - } + Image image = BrowserSelectionListComp.snapshot(FXCollections.observableList(selected)); + db.setDragView(image, -20, 15); - // 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 (syncDownloaded.getValue()) { - return; - } + event.setDragDetect(true); + event.consume(); + }); + struc.get().setOnDragDone(event -> { + if (!event.isAccepted()) { + return; + } - var selected = syncItems.stream() - .map(item -> item.getBrowserEntry()) - .toList(); - Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY); - - var cc = BrowserClipboard.startDrag(null, selected, BrowserFileTransferMode.NORMAL); - if (cc == null) { - return; - } - - var files = syncItems.stream() - .filter(item -> item.downloadFinished().get()) - .map(item -> { - try { - var file = item.getLocalFile(); - if (!Files.exists(file)) { - return Optional.empty(); - } - - return Optional.of( - file.toRealPath().toFile()); - } catch (IOException e) { - throw new RuntimeException(e); - } - }) - .flatMap(Optional::stream) - .toList(); - cc.putFiles(files); - db.setContent(cc); - - Image image = BrowserSelectionListComp.snapshot(FXCollections.observableList(selected)); - db.setDragView(image, -20, 15); - - event.setDragDetect(true); - event.consume(); - }); - struc.get().setOnDragDone(event -> { - if (!event.isAccepted()) { - return; - } - - // The files might not have been transferred yet - // We can't listen to this, so just don't delete them - model.clear(false); - event.consume(); - }); - }), - syncDownloaded); + // The files might not have been transferred yet + // We can't listen to this, so just don't delete them + model.clear(false); + event.consume(); + }); + }); stack.apply(struc -> { model.getBrowserSessionModel().getDraggingFiles().addListener((observable, oldValue, newValue) -> { - struc.get().pseudoClassStateChanged(PseudoClass.getPseudoClass("highlighted"),newValue); + struc.get().pseudoClassStateChanged(PseudoClass.getPseudoClass("highlighted"), newValue); }); }); 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 bc19c109b..4a89c7e07 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserTransferModel.java @@ -7,13 +7,12 @@ import io.xpipe.app.browser.file.LocalFileSystem; import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.browser.session.BrowserSessionModel; import io.xpipe.app.issue.ErrorEvent; -import io.xpipe.app.util.BooleanScope; +import io.xpipe.app.util.DesktopHelper; import io.xpipe.app.util.ShellTemp; +import io.xpipe.app.util.ThreadHelper; 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; @@ -22,136 +21,156 @@ 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.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.Optional; @Value public class BrowserTransferModel { private static final Path TEMP = ShellTemp.getLocalTempDataDirectory("download"); - ExecutorService executor = Executors.newSingleThreadExecutor(r -> { - Thread t = Executors.defaultThreadFactory().newThread(r); - t.setDaemon(true); - t.setName("file downloader"); - return t; - }); BrowserSessionModel browserSessionModel; ObservableList items = FXCollections.observableArrayList(); - BooleanProperty downloading = new SimpleBooleanProperty(); - BooleanProperty allDownloaded = new SimpleBooleanProperty(); + ObservableBooleanValue empty = Bindings.createBooleanBinding(() -> items.isEmpty(), items); - private void cleanDirectory() { + public BrowserTransferModel(BrowserSessionModel browserSessionModel) { + this.browserSessionModel = browserSessionModel; + var thread = ThreadHelper.createPlatformThread("file downloader", true, () -> { + while (true) { + Optional toDownload; + synchronized (items) { + toDownload = items.stream() + .filter(item -> !item.downloadFinished().get()) + .findFirst(); + } + if (toDownload.isPresent()) { + downloadSingle(toDownload.get()); + } + ThreadHelper.sleep(20); + } + }); + thread.start(); + } + + public List getCurrentItems() { + synchronized (items) { + return new ArrayList<>(items); + } + } + + private void cleanItem(Item item) { if (!Files.isDirectory(TEMP)) { return; } - try (var ls = Files.list(TEMP)) { - var list = ls.toList(); - for (Path path : list) { - FileUtils.forceDelete(path.toFile()); - } + if (!Files.exists(item.getLocalFile())) { + return; + } + + try { + FileUtils.forceDelete(item.getLocalFile().toFile()); } catch (IOException e) { ErrorEvent.fromThrowable(e).handle(); } } public void clear(boolean delete) { - items.clear(); + List toClear; + synchronized (items) { + toClear = + items.stream().filter(item -> item.downloadFinished().get()).toList(); + if (toClear.isEmpty()) { + return; + } + items.removeAll(toClear); + } if (delete) { - executor.submit(() -> { - cleanDirectory(); - }); + for (Item item : toClear) { + cleanItem(item); + } } } public void drop(OpenFileSystemModel model, List entries) { - entries.forEach(entry -> { - var name = entry.getFileName(); - if (items.stream().anyMatch(item -> item.getName().equals(name))) { - return; - } - - Path file = TEMP.resolve(name); - 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 = LocalFileSystem.getLocalBrowserEntry(path); + synchronized (items) { + entries.forEach(entry -> { var name = entry.getFileName(); if (items.stream().anyMatch(item -> item.getName().equals(name))) { return; } - var item = new Item(null, name, entry, path); - item.progress.setValue(BrowserTransferProgress.finished( - entry.getFileName(), entry.getRawFileEntry().getSize())); + Path file = TEMP.resolve(name); + var item = new Item(model, name, entry, file); items.add(item); - } - } catch (Exception ex) { - ErrorEvent.fromThrowable(ex).handle(); - } - if (empty) { - allDownloaded.set(true); + }); } } - public void download() { - executor.submit(() -> { - try { - FileUtils.forceMkdir(TEMP.toFile()); - } catch (IOException e) { - ErrorEvent.fromThrowable(e).handle(); + public void downloadSingle(Item item) { + try { + FileUtils.forceMkdir(TEMP.toFile()); + } catch (IOException e) { + ErrorEvent.fromThrowable(e).handle(); + return; + } + + if (item.downloadFinished().get()) { + return; + } + + if (item.getOpenFileSystemModel() != null + && item.getOpenFileSystemModel().isClosed()) { + return; + } + + try { + var op = new BrowserFileTransferOperation( + LocalFileSystem.getLocalFileEntry(TEMP), + List.of(item.getBrowserEntry().getRawFileEntry()), + BrowserFileTransferMode.COPY, + false, + progress -> { + synchronized (item.getProgress()) { + item.getProgress().setValue(progress); + } + item.getOpenFileSystemModel().getProgress().setValue(progress); + }); + op.execute(); + } catch (Throwable t) { + ErrorEvent.fromThrowable(t).handle(); + synchronized (items) { + items.remove(item); + } + } + } + + public void transferToDownloads() throws Exception { + List toMove; + synchronized (items) { + toMove = + items.stream().filter(item -> item.downloadFinished().get()).toList(); + if (toMove.isEmpty()) { return; } + items.removeAll(toMove); + } - for (Item item : new ArrayList<>(items)) { - if (item.downloadFinished().get()) { - continue; - } - - if (item.getOpenFileSystemModel() != null - && item.getOpenFileSystemModel().isClosed()) { - continue; - } - - try { - try (var ignored = new BooleanScope(downloading).start()) { - var op = new BrowserFileTransferOperation( - LocalFileSystem.getLocalFileEntry(TEMP), - List.of(item.getBrowserEntry().getRawFileEntry()), - BrowserFileTransferMode.COPY, - false, - progress -> { - item.getProgress().setValue(progress); - item.getOpenFileSystemModel().getProgress().setValue(progress); - }); - op.execute(); - } - } catch (Throwable t) { - ErrorEvent.fromThrowable(t).handle(); - items.remove(item); - } + var files = toMove.stream().map(item -> item.getLocalFile()).toList(); + var downloads = DesktopHelper.getDownloadsDirectory(); + for (Path file : files) { + var target = downloads.resolve(file.getFileName()); + // Prevent DirectoryNotEmptyException + if (Files.exists(target) && Files.isDirectory(target)) { + Files.delete(target); } - allDownloaded.set(true); - }); + Files.move(file, target, StandardCopyOption.REPLACE_EXISTING); + } + DesktopHelper.browseFileInDirectory(downloads.resolve(files.getFirst().getFileName())); } @Value @@ -171,12 +190,11 @@ public class BrowserTransferModel { } public ObservableBooleanValue downloadFinished() { - return Bindings.createBooleanBinding( - () -> { - return progress.getValue() != null - && progress.getValue().done(); - }, - progress); + synchronized (progress) { + return Bindings.createBooleanBinding(() -> { + return progress.getValue() != null && 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 index 5deb42634..bb1e16ec7 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserTransferProgress.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserTransferProgress.java @@ -41,6 +41,7 @@ public class BrowserTransferProgress { var share = (double) transferred / total; var rest = (1.0 - share) / share; var restMillis = (long) (elapsed.toMillis() * rest); - return Duration.of(restMillis, ChronoUnit.MILLIS); + var startupAdjustment = (long) (restMillis / (1.0 + Math.max(10000 - elapsed.toMillis(), 0) / 10000.0)); + return Duration.of(restMillis + startupAdjustment, ChronoUnit.MILLIS); } } 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 8350041df..08f4f4d4a 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java @@ -45,7 +45,7 @@ public class BrowserWelcomeComp extends SimpleComp { @Override protected Region createSimple() { - var state = model.getSavedState(); + var state = BrowserSavedStateImpl.get(); var welcome = new BrowserGreetingComp().createSimple(); @@ -55,6 +55,7 @@ public class BrowserWelcomeComp extends SimpleComp { var img = new PrettySvgComp(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); @@ -139,7 +140,6 @@ public class BrowserWelcomeComp extends SimpleComp { .hide(empty) .accessibleTextKey("restoreAllSessions"); layout.getChildren().add(tile.createRegion()); - return layout; } @@ -149,7 +149,7 @@ public class BrowserWelcomeComp extends SimpleComp { entry.get().getProvider().getDisplayIconFileName(entry.get().getStore()); var view = PrettyImageHelper.ofFixedSize(graphic, 30, 24); return new ButtonComp( - new SimpleStringProperty(DataStorage.get().getStoreDisplayName(entry.get())), + new SimpleStringProperty(DataStorage.get().getStoreEntryDisplayName(entry.get())), view.createRegion(), () -> { ThreadHelper.runAsync(() -> { @@ -160,7 +160,7 @@ public class BrowserWelcomeComp extends SimpleComp { }); }) .minWidth(250) - .accessibleText(DataStorage.get().getStoreDisplayName(entry.get())) + .accessibleText(DataStorage.get().getStoreEntryDisplayName(entry.get())) .disable(disable) .styleClass("entry-button") .styleClass(Styles.LEFT_PILL) diff --git a/app/src/main/java/io/xpipe/app/browser/action/BranchAction.java b/app/src/main/java/io/xpipe/app/browser/action/BranchAction.java index 6d77f5a5a..4c722f406 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/BranchAction.java +++ b/app/src/main/java/io/xpipe/app/browser/action/BranchAction.java @@ -2,10 +2,40 @@ package io.xpipe.app.browser.action; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.fs.OpenFileSystemModel; +import io.xpipe.app.util.LicenseProvider; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; +import org.kordamp.ikonli.javafx.FontIcon; import java.util.List; public interface BranchAction extends BrowserAction { - List getBranchingActions(OpenFileSystemModel model, List entries); + default MenuItem toMenuItem(OpenFileSystemModel model, List selected) { + var m = new Menu(getName(model, selected).getValue() + " ..."); + for (var sub : getBranchingActions(model, selected)) { + var subselected = resolveFilesIfNeeded(selected); + if (!sub.isApplicable(model, subselected)) { + continue; + } + m.getItems().add(sub.toMenuItem(model, subselected)); + } + var graphic = getIcon(model, selected); + if (graphic != null) { + m.setGraphic(graphic); + } + m.setDisable(!isActive(model, selected)); + + if (getProFeatureId() != null + && !LicenseProvider.get() + .getFeature(getProFeatureId()) + .isSupported()) { + m.setDisable(true); + m.setGraphic(new FontIcon("mdi2p-professional-hexagon")); + } + + return m; + } + + List getBranchingActions(OpenFileSystemModel model, List entries); } 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 285959aa1..5387d3c08 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 @@ -7,6 +7,7 @@ import io.xpipe.core.util.ModuleLayerLoader; import javafx.beans.value.ObservableValue; import javafx.scene.Node; +import javafx.scene.control.MenuItem; import javafx.scene.input.KeyCombination; import java.util.ArrayList; @@ -19,13 +20,17 @@ public interface BrowserAction { static List getFlattened(OpenFileSystemModel model, List entries) { return ALL.stream() - .map(browserAction -> browserAction instanceof LeafAction - ? List.of((LeafAction) browserAction) - : ((BranchAction) browserAction).getBranchingActions(model, entries)) + .map(browserAction -> getFlattened(browserAction, model, entries)) .flatMap(List::stream) .toList(); } + static List getFlattened(BrowserAction browserAction, OpenFileSystemModel model, List entries) { + return browserAction instanceof LeafAction + ? List.of((LeafAction) browserAction) + : ((BranchAction) browserAction).getBranchingActions(model, entries).stream().map(action -> getFlattened(action, model, entries)).flatMap(List::stream).toList(); + } + static LeafAction byId(String id, OpenFileSystemModel model, List entries) { return getFlattened(model, entries).stream() .filter(browserAction -> id.equals(browserAction.getId())) @@ -33,6 +38,17 @@ public interface BrowserAction { .orElseThrow(); } + default List resolveFilesIfNeeded(List selected) { + return automaticallyResolveLinks() + ? selected.stream() + .map(browserEntry -> + new BrowserEntry(browserEntry.getRawFileEntry().resolved(), browserEntry.getModel())) + .toList() + : selected; + } + + MenuItem toMenuItem(OpenFileSystemModel model, List selected); + default void init(OpenFileSystemModel model) throws Exception {} default String getProFeatureId() { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserAlerts.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserAlerts.java index 3700b17eb..3ea4510b9 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserAlerts.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserAlerts.java @@ -80,7 +80,10 @@ public class BrowserAlerts { private static String getSelectedElementsString(List source) { var namesHeader = AppI18n.get("selectedElements"); var names = namesHeader + "\n" - + source.stream().limit(10).map(entry -> "- " + new FilePath(entry.getPath()).getFileName()).collect(Collectors.joining("\n")); + + source.stream() + .limit(10) + .map(entry -> "- " + new FilePath(entry.getPath()).getFileName()) + .collect(Collectors.joining("\n")); if (source.size() > 10) { names += "\n+ " + (source.size() - 10) + " ..."; } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserContextMenu.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserContextMenu.java index 6e33b03b8..bca81c851 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserContextMenu.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserContextMenu.java @@ -1,19 +1,12 @@ package io.xpipe.app.browser.file; -import io.xpipe.app.browser.action.BranchAction; import io.xpipe.app.browser.action.BrowserAction; -import io.xpipe.app.browser.action.LeafAction; import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.core.AppFont; import io.xpipe.app.util.InputHelper; -import io.xpipe.app.util.LicenseProvider; - import javafx.scene.control.ContextMenu; -import javafx.scene.control.Menu; import javafx.scene.control.SeparatorMenuItem; -import org.kordamp.ikonli.javafx.FontIcon; - import java.util.ArrayList; import java.util.List; @@ -30,15 +23,6 @@ public 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())) - .toList() - : selected; - } - private void createMenu() { InputHelper.onLeft(this, false, e -> { hide(); @@ -60,7 +44,7 @@ public final class BrowserContextMenu extends ContextMenu { var all = BrowserAction.ALL.stream() .filter(browserAction -> browserAction.getCategory() == cat) .filter(browserAction -> { - var used = resolveIfNeeded(browserAction, selected); + var used = browserAction.resolveFilesIfNeeded(selected); if (!browserAction.isApplicable(model, used)) { return false; } @@ -81,36 +65,8 @@ public final class BrowserContextMenu extends ContextMenu { } for (BrowserAction a : all) { - var used = resolveIfNeeded(a, selected); - if (a instanceof LeafAction la) { - getItems().add(la.toMenuItem(model, used)); - } - - if (a instanceof BranchAction la) { - var m = new Menu(a.getName(model, used).getValue() + " ..."); - for (LeafAction sub : la.getBranchingActions(model, used)) { - var subUsed = resolveIfNeeded(sub, selected); - if (!sub.isApplicable(model, subUsed)) { - continue; - } - m.getItems().add(sub.toMenuItem(model, subUsed)); - } - var graphic = a.getIcon(model, used); - if (graphic != null) { - m.setGraphic(graphic); - } - m.setDisable(!a.isActive(model, used)); - - if (la.getProFeatureId() != null - && !LicenseProvider.get() - .getFeature(la.getProFeatureId()) - .isSupported()) { - m.setDisable(true); - m.setGraphic(new FontIcon("mdi2p-professional-hexagon")); - } - - getItems().add(m); - } + var used = a.resolveFilesIfNeeded(selected); + getItems().add(a.toMenuItem(model, used)); } } } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java index 052052b84..091667fc7 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java @@ -4,8 +4,6 @@ import io.xpipe.app.browser.action.BrowserAction; import io.xpipe.app.comp.base.LazyTextFieldComp; import io.xpipe.app.core.AppI18n; 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.PrettyImageHelper; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.util.*; @@ -29,10 +27,7 @@ import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.control.skin.TableViewSkin; import javafx.scene.control.skin.VirtualFlow; -import javafx.scene.input.DragEvent; -import javafx.scene.input.KeyCode; -import javafx.scene.input.MouseButton; -import javafx.scene.input.MouseEvent; +import javafx.scene.input.*; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; @@ -40,11 +35,13 @@ import javafx.scene.layout.Region; import atlantafx.base.controls.Spacer; import atlantafx.base.theme.Styles; +import java.time.Duration; import java.time.Instant; import java.time.ZoneId; import java.util.ArrayList; import java.util.Comparator; import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; import static io.xpipe.app.util.HumanReadableFormat.byteCount; import static javafx.scene.control.TableColumn.SortType.ASCENDING; @@ -60,6 +57,7 @@ public final class BrowserFileListComp extends SimpleComp { private static final PseudoClass DRAG_INTO_CURRENT = PseudoClass.getPseudoClass("drag-into-current"); private final BrowserFileListModel fileList; + private final StringProperty typedSelection = new SimpleStringProperty(""); public BrowserFileListComp(BrowserFileListModel fileList) { this.fileList = fileList; @@ -124,16 +122,80 @@ public final class BrowserFileListComp extends SimpleComp { return true; }); table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN); - table.setFixedCellSize(34.0); + table.setFixedCellSize(32.0); prepareTableSelectionModel(table); prepareTableShortcuts(table); prepareTableEntries(table); prepareTableChanges(table, mtimeCol, modeCol); + prepareTypedSelectionModel(table); return table; } + private void prepareTypedSelectionModel(TableView table) { + AtomicReference lastFail = new AtomicReference<>(); + table.addEventHandler(KeyEvent.KEY_PRESSED, event -> { + updateTypedSelection(table, lastFail, event, false); + }); + + table.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> { + typedSelection.set(""); + lastFail.set(null); + }); + + fileList.getFileSystemModel().getCurrentPath().addListener((observable, oldValue, newValue) -> { + typedSelection.set(""); + lastFail.set(null); + }); + + table.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + if (event.getCode() == KeyCode.ESCAPE) { + typedSelection.set(""); + lastFail.set(null); + } + }); + } + + private void updateTypedSelection(TableView table, AtomicReference lastType, KeyEvent event, boolean recursive) { + var typed = event.getText(); + if (typed.isEmpty()) { + return; + } + + var updated = typedSelection.get() + typed; + var found = fileList.getShown().getValue().stream() + .filter(browserEntry -> + browserEntry.getFileName().toLowerCase().startsWith(updated.toLowerCase())) + .findFirst(); + if (found.isEmpty()) { + if (typedSelection.get().isEmpty()) { + return; + } + + var inCooldown = lastType.get() != null && Duration.between(lastType.get(), Instant.now()).toMillis() < 1000; + if (inCooldown) { + lastType.set(Instant.now()); + event.consume(); + return; + } else { + lastType.set(null); + typedSelection.set(""); + table.getSelectionModel().clearSelection(); + if (!recursive) { + updateTypedSelection(table, lastType, event, true); + } + return; + } + } + + lastType.set(Instant.now()); + typedSelection.set(updated); + table.scrollTo(found.get()); + table.getSelectionModel().clearAndSelect(fileList.getShown().getValue().indexOf(found.get())); + event.consume(); + } + private void prepareTableSelectionModel(TableView table) { if (!fileList.getSelectionMode().isMultiple()) { table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); @@ -167,7 +229,7 @@ public final class BrowserFileListComp extends SimpleComp { } private void prepareTableShortcuts(TableView table) { - table.setOnKeyPressed(event -> { + table.addEventFilter(KeyEvent.KEY_PRESSED, event -> { var selected = fileList.getSelection(); var action = BrowserAction.getFlattened(fileList.getFileSystemModel(), selected).stream() .filter(browserAction -> browserAction.isApplicable(fileList.getFileSystemModel(), selected) @@ -219,7 +281,6 @@ public final class BrowserFileListComp extends SimpleComp { emptyEntry.onDragDone(event); }); - // Don't let the list view see this event // otherwise it unselects everything as it doesn't understand shift clicks table.addEventFilter(MouseEvent.MOUSE_CLICKED, t -> { @@ -242,38 +303,6 @@ public final class BrowserFileListComp extends SimpleComp { return row.getItem() != null; }, row.itemProperty())); - new ContextMenuAugment<>( - event -> { - if (row.getItem() == null) { - return event.getButton() == MouseButton.SECONDARY; - } - - if (row.getItem() != null - && row.getItem() - .getRawFileEntry() - .resolved() - .getKind() - == FileKind.DIRECTORY) { - return event.getButton() == MouseButton.SECONDARY; - } - - if (row.getItem() != null - && row.getItem() - .getRawFileEntry() - .resolved() - .getKind() - != FileKind.DIRECTORY) { - return event.getButton() == MouseButton.SECONDARY - || event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2; - } - - return false; - }, - null, - () -> { - return new BrowserContextMenu(fileList.getFileSystemModel(), row.getItem(), false); - }) - .augment(new SimpleCompStructure<>(row)); var listEntry = Bindings.createObjectBinding( () -> new BrowserFileListCompEntry(table, row, row.getItem(), fileList), row.itemProperty()); @@ -332,7 +361,6 @@ public final class BrowserFileListComp extends SimpleComp { listEntry.get().onDragDone(event); }); - return row; }); } @@ -564,7 +592,18 @@ public final class BrowserFileListComp extends SimpleComp { event.consume(); } }); - InputHelper.onExactKeyCode(tableView, KeyCode.SPACE, false, event -> { + InputHelper.onExactKeyCode(tableView, KeyCode.SPACE, true, event -> { + var selection = typedSelection.get() + " "; + var found = fileList.getShown().getValue().stream() + .filter(browserEntry -> + browserEntry.getFileName().toLowerCase().startsWith(selection)) + .findFirst(); + // Ugly fix to prevent space from showing the menu when there is a file matching + // Due to the table view input map, these events always get sent and consumed, not allowing us to differentiate between these cases + if (found.isPresent()) { + return; + } + var selected = fileList.getSelection(); // Only show one menu across all selected entries if (selected.size() > 0 && selected.getLast() == getTableRow().getItem()) { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListCompEntry.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListCompEntry.java index 4ee3fbd4b..7bc1e3485 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListCompEntry.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListCompEntry.java @@ -7,6 +7,7 @@ import io.xpipe.core.store.FileKind; import javafx.geometry.Point2D; import javafx.scene.Node; +import javafx.scene.control.ContextMenu; import javafx.scene.control.TableView; import javafx.scene.image.Image; import javafx.scene.input.*; @@ -31,6 +32,7 @@ public class BrowserFileListCompEntry { private Point2D lastOver = new Point2D(-1, -1); private TimerTask activeTask; + private ContextMenu lastContextMenu; public BrowserFileListCompEntry( TableView tv, Node row, BrowserEntry item, BrowserFileListModel model) { @@ -41,6 +43,19 @@ public class BrowserFileListCompEntry { } public void onMouseClick(MouseEvent t) { + if (lastContextMenu != null) { + lastContextMenu.hide(); + lastContextMenu = null; + } + + if (showContextMenu(t)) { + var cm = new BrowserContextMenu(model.getFileSystemModel(), item, false); + cm.show(row, t.getScreenX(), t.getScreenY()); + lastContextMenu = cm; + t.consume(); + return; + } + if (item == null) { // Only clear for normal clicks if (t.isStillSincePress()) { @@ -62,6 +77,23 @@ public class BrowserFileListCompEntry { t.consume(); } + private boolean showContextMenu(MouseEvent event) { + if (item == null) { + return event.getButton() == MouseButton.SECONDARY; + } + + if (item.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) { + return event.getButton() == MouseButton.SECONDARY; + } + + if (item.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY) { + return event.getButton() == MouseButton.SECONDARY + || event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2; + } + + return false; + } + public void onMouseShiftClick(MouseEvent t) { if (t.getButton() != MouseButton.PRIMARY) { return; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java index 03514d0c0..676d11174 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java @@ -99,24 +99,32 @@ public final class BrowserFileListModel { } public BrowserEntry rename(BrowserEntry old, String newName) { + if (fileSystemModel == null || fileSystemModel.isClosed() || fileSystemModel.getCurrentPath().get() == null) { + return old; + } + var fullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), old.getFileName()); var newFullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), newName); // This check will fail on case-insensitive file systems when changing the case of the file // So skip it in this case - var skipExistCheck = fileSystemModel.getFileSystem().getShell().orElseThrow().getOsType() == OsType.WINDOWS && old.getFileName() - .equalsIgnoreCase(newName); + var skipExistCheck = + fileSystemModel.getFileSystem().getShell().orElseThrow().getOsType() == OsType.WINDOWS + && old.getFileName().equalsIgnoreCase(newName); if (!skipExistCheck) { boolean exists; try { - exists = fileSystemModel.getFileSystem().fileExists(newFullPath) || fileSystemModel.getFileSystem().directoryExists(newFullPath); + exists = fileSystemModel.getFileSystem().fileExists(newFullPath) + || fileSystemModel.getFileSystem().directoryExists(newFullPath); } catch (Exception e) { ErrorEvent.fromThrowable(e).handle(); return old; } if (exists) { - ErrorEvent.fromMessage("Target " + newFullPath + " does already exist").expected().handle(); + ErrorEvent.fromMessage("Target " + newFullPath + " does already exist") + .expected() + .handle(); fileSystemModel.refresh(); return old; } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java index a385bec1e..640d7c868 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java @@ -7,9 +7,7 @@ import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FilePath; import io.xpipe.core.store.FileSystem; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; @@ -220,62 +218,85 @@ public class BrowserFileTransferOperation { 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, start); - inputStream.transferTo(OutputStream.nullOutputStream()); - } catch (Exception ex) { - // Mark progress as finished to reset any progress display - updateProgress(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; - } + transfer(sourceFile, targetFile, transferred, totalSize, start); } } updateProgress(BrowserTransferProgress.finished(source.getName(), totalSize.get())); } + private void transfer( + FileSystem.FileEntry sourceFile, + String targetFile, + AtomicLong transferred, + AtomicLong totalSize, + Instant start) + throws Exception { + InputStream inputStream = null; + OutputStream outputStream = null; + try { + var fileSize = sourceFile.getFileSystem().getFileSize(sourceFile.getPath()); + + // Read the first few bytes to figure out possible command failure early + // before creating the output stream + inputStream = new BufferedInputStream(sourceFile.getFileSystem().openInput(sourceFile.getPath()), 1024); + inputStream.mark(1024); + var streamStart = new byte[1024]; + var streamStartLength = inputStream.read(streamStart, 0, 1024); + if (streamStartLength < 1024) { + inputStream.close(); + inputStream = new ByteArrayInputStream(streamStart); + } else { + inputStream.reset(); + } + + outputStream = target.getFileSystem().openOutput(targetFile, fileSize); + transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, start); + inputStream.transferTo(OutputStream.nullOutputStream()); + } catch (Exception ex) { + // Mark progress as finished to reset any progress display + updateProgress(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; + } + } + private void deleteSingle(FileSystem.FileEntry source) throws Exception { source.getFileSystem().delete(source.getPath()); } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java index dc4f4b3f1..a1c0d3ba1 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java @@ -79,7 +79,7 @@ public class BrowserQuickAccessContextMenu extends ContextMenu { getItems().addAll(r.getItems()); // Prevent NPE in show() - if (getScene() == null) { + if (getScene() == null || anchor == null) { return; } show(anchor, Side.RIGHT, 0, 0); diff --git a/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemComp.java b/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemComp.java index 526699132..145158c02 100644 --- a/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemComp.java +++ b/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemComp.java @@ -9,6 +9,7 @@ import io.xpipe.app.browser.file.BrowserContextMenu; import io.xpipe.app.browser.file.BrowserFileListComp; import io.xpipe.app.comp.base.ModalOverlayComp; import io.xpipe.app.comp.base.MultiContentComp; +import io.xpipe.app.core.AppFont; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleCompStructure; @@ -84,10 +85,11 @@ public class OpenFileSystemComp extends SimpleComp { var filter = new BrowserFilterComp(model, model.getFilter()).createStructure(); var topBar = new HBox(); - filter.textField().prefHeightProperty().bind(topBar.heightProperty()); topBar.setAlignment(Pos.CENTER); topBar.getStyleClass().add("top-bar"); var navBar = new BrowserNavBar(model).createStructure(); + filter.textField().prefHeightProperty().bind(navBar.get().heightProperty()); + AppFont.medium(navBar.get()); topBar.getChildren() .setAll( overview, @@ -117,13 +119,13 @@ public class OpenFileSystemComp extends SimpleComp { }); InputHelper.onKeyCombination( - root, new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN), true, keyEvent -> { + root, new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN), true, keyEvent -> { filter.toggleButton().fire(); filter.textField().requestFocus(); keyEvent.consume(); }); InputHelper.onKeyCombination( - root, new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN), true, keyEvent -> { + root, new KeyCodeCombination(KeyCode.L, KeyCombination.SHORTCUT_DOWN), true, keyEvent -> { navBar.textField().requestFocus(); keyEvent.consume(); }); @@ -140,6 +142,14 @@ public class OpenFileSystemComp extends SimpleComp { } keyEvent.consume(); }); + InputHelper.onKeyCombination( + root, new KeyCodeCombination(KeyCode.BACK_SPACE), true, keyEvent -> { + var p = model.getCurrentParentDirectory(); + if (p != null) { + model.cdAsync(p.getPath()); + } + keyEvent.consume(); + }); return root; } diff --git a/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemModel.java b/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemModel.java index 7067f8e98..824557bb2 100644 --- a/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemModel.java +++ b/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemModel.java @@ -1,6 +1,7 @@ package io.xpipe.app.browser.fs; import io.xpipe.app.browser.BrowserSavedState; +import io.xpipe.app.browser.BrowserSavedStateImpl; import io.xpipe.app.browser.BrowserTransferProgress; import io.xpipe.app.browser.action.BrowserAction; import io.xpipe.app.browser.file.BrowserFileListModel; @@ -8,7 +9,6 @@ import io.xpipe.app.browser.file.BrowserFileTransferMode; import io.xpipe.app.browser.file.BrowserFileTransferOperation; import io.xpipe.app.browser.file.FileSystemHelper; import io.xpipe.app.browser.session.BrowserAbstractSessionModel; -import io.xpipe.app.browser.session.BrowserSessionModel; import io.xpipe.app.browser.session.BrowserSessionTab; import io.xpipe.app.comp.base.ModalOverlayComp; import io.xpipe.app.fxcomps.Comp; @@ -110,14 +110,13 @@ public final class OpenFileSystemModel extends BrowserSessionTab { - if (model.isClosed()) { - return; - } - if (Objects.equals(lastDirectory, dir)) { - updateRecent(dir); - save(); - } - }); - } - }, - 10000); + if (delay) { + // 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); + } else { + updateRecent(dir); + save(); + } } private void updateRecent(String dir) { diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserAbstractSessionModel.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserAbstractSessionModel.java index 289d4cf46..980f13871 100644 --- a/app/src/main/java/io/xpipe/app/browser/session/BrowserAbstractSessionModel.java +++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserAbstractSessionModel.java @@ -17,6 +17,7 @@ public class BrowserAbstractSessionModel> { protected final ObservableList sessionEntries = FXCollections.observableArrayList(); protected final Property selectedEntry = new SimpleObjectProperty<>(); + protected final BooleanProperty busy = new SimpleBooleanProperty(); public void closeAsync(BrowserSessionTab e) { ThreadHelper.runAsync(() -> { diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserChooserComp.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserChooserComp.java index 9eb4d19ee..8b38c6bb9 100644 --- a/app/src/main/java/io/xpipe/app/browser/session/BrowserChooserComp.java +++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserChooserComp.java @@ -67,6 +67,10 @@ public class BrowserChooserComp extends SimpleComp { window.close(); }); window.show(); + window.setOnHidden(event -> { + model.finishWithoutChoice(); + event.consume(); + }); ThreadHelper.runAsync(() -> { model.openFileSystemAsync(store.get(), null, null); }); diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserFileChooserModel.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserFileChooserModel.java index 6bfbd5ff6..162fe3c6d 100644 --- a/app/src/main/java/io/xpipe/app/browser/session/BrowserFileChooserModel.java +++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserFileChooserModel.java @@ -65,6 +65,17 @@ public class BrowserFileChooserModel extends BrowserAbstractSessionModel { + open.close(); + }); + } + } + } + public void openFileSystemAsync( DataStoreEntryRef store, FailableFunction path, diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionComp.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionComp.java index ae37e2c6b..ed51d4995 100644 --- a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionComp.java +++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionComp.java @@ -3,10 +3,13 @@ package io.xpipe.app.browser.session; import io.xpipe.app.browser.BrowserBookmarkComp; import io.xpipe.app.browser.BrowserBookmarkHeaderComp; import io.xpipe.app.browser.BrowserTransferComp; +import io.xpipe.app.comp.base.LoadingOverlayComp; import io.xpipe.app.comp.base.SideSplitPaneComp; import io.xpipe.app.comp.store.StoreEntryWrapper; import io.xpipe.app.core.AppLayoutModel; +import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; +import io.xpipe.app.fxcomps.impl.AnchorComp; import io.xpipe.app.fxcomps.impl.StackComp; import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.util.BindingsHelper; @@ -18,6 +21,7 @@ import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleDoubleProperty; +import javafx.scene.layout.AnchorPane; import javafx.scene.layout.Region; import javafx.scene.shape.Rectangle; @@ -100,10 +104,22 @@ public class BrowserSessionComp extends SimpleComp { new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer, localDownloadStage)).styleClass("left"); var split = new SimpleDoubleProperty(); - var tabs = new BrowserSessionTabsComp(model, split) - .apply(struc -> struc.get().setViewOrder(1)) - .apply(struc -> struc.get().setPickOnBounds(false)); - var splitPane = new SideSplitPaneComp(vertical, tabs) + var tabs = new BrowserSessionTabsComp(model, split).apply(struc -> { + struc.get().setViewOrder(1); + struc.get().setPickOnBounds(false); + AnchorPane.setTopAnchor(struc.get(), 0.0); + AnchorPane.setBottomAnchor(struc.get(), 0.0); + AnchorPane.setLeftAnchor(struc.get(), 0.0); + AnchorPane.setRightAnchor(struc.get(), 0.0); + }); + var loadingIndicator = LoadingOverlayComp.noProgress(Comp.empty(), model.getBusy()) + .apply(struc -> { + AnchorPane.setTopAnchor(struc.get(), 0.0); + AnchorPane.setRightAnchor(struc.get(), 0.0); + }) + .styleClass("tab-loading-indicator"); + var loadingStack = new AnchorComp(List.of(tabs, loadingIndicator)); + var splitPane = new SideSplitPaneComp(vertical, loadingStack) .withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth()) .withOnDividerChange(d -> { AppLayoutModel.get().getSavedState().setBrowserConnectionsWidth(d); diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionModel.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionModel.java index 59fe6101f..ba254d928 100644 --- a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionModel.java +++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionModel.java @@ -23,16 +23,11 @@ import java.util.ArrayList; @Getter public class BrowserSessionModel extends BrowserAbstractSessionModel> { - public static final BrowserSessionModel DEFAULT = new BrowserSessionModel(BrowserSavedStateImpl.load()); + public static final BrowserSessionModel DEFAULT = new BrowserSessionModel(); private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this); - private final BrowserSavedState savedState; private final Property draggingFiles = new SimpleBooleanProperty(); - public BrowserSessionModel(BrowserSavedState savedState) { - this.savedState = savedState; - } - public void restoreState(BrowserSavedState state) { ThreadHelper.runAsync(() -> { var l = new ArrayList<>(state.getEntries()); @@ -62,9 +57,7 @@ public class BrowserSessionModel extends BrowserAbstractSessionModel store, FailableFunction path, - BooleanProperty externalBusy) throws Exception { + BooleanProperty externalBusy) + throws Exception { if (store == null) { return; } OpenFileSystemModel model; try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) { - model = new OpenFileSystemModel(this, store, OpenFileSystemModel.SelectionMode.ALL); - model.init(); - // Prevent multiple calls from interfering with each other - synchronized (BrowserSessionModel.this) { - sessionEntries.add(model); - // The tab pane doesn't automatically select new tabs - selectedEntry.setValue(model); + try (var sessionBusy = new BooleanScope(busy).exclusive().start()) { + model = new OpenFileSystemModel(this, store, OpenFileSystemModel.SelectionMode.ALL); + model.init(); + // Prevent multiple calls from interfering with each other + synchronized (BrowserSessionModel.this) { + sessionEntries.add(model); + // The tab pane doesn't automatically select new tabs + selectedEntry.setValue(model); + } } } if (path != null) { diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTab.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTab.java index 64414f537..09bda38fa 100644 --- a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTab.java +++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTab.java @@ -22,7 +22,7 @@ public abstract class BrowserSessionTab { public BrowserSessionTab(BrowserAbstractSessionModel browserModel, DataStoreEntryRef entry) { this.browserModel = browserModel; this.entry = entry; - this.name = DataStorage.get().getStoreDisplayName(entry.get()); + this.name = DataStorage.get().getStoreEntryDisplayName(entry.get()); this.tooltip = DataStorage.get().getStorePath(entry.getEntry()).toString(); } diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTabsComp.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTabsComp.java index 907ead2c8..8cef777bd 100644 --- a/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTabsComp.java +++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserSessionTabsComp.java @@ -1,16 +1,19 @@ package io.xpipe.app.browser.session; +import atlantafx.base.controls.RingProgressIndicator; +import atlantafx.base.theme.Styles; import io.xpipe.app.browser.BrowserWelcomeComp; import io.xpipe.app.comp.base.MultiContentComp; +import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.TooltipAugment; +import io.xpipe.app.fxcomps.util.LabelGraphic; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.util.BooleanScope; -import io.xpipe.app.util.InputHelper; - +import io.xpipe.app.util.ContextMenuHelper; import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleBooleanProperty; @@ -20,20 +23,12 @@ import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.control.Label; -import javafx.scene.control.Tab; -import javafx.scene.control.TabPane; -import javafx.scene.input.DragEvent; -import javafx.scene.input.KeyCode; +import javafx.scene.control.*; +import javafx.scene.input.*; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; -import atlantafx.base.controls.RingProgressIndicator; -import atlantafx.base.theme.Styles; - -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; +import java.util.*; import static atlantafx.base.theme.Styles.DENSE; import static atlantafx.base.theme.Styles.toggleStyleClass; @@ -50,17 +45,17 @@ public class BrowserSessionTabsComp extends SimpleComp { } public Region createSimple() { - var multi = new MultiContentComp(Map., ObservableValue>of( - Comp.of(() -> createTabPane()), - Bindings.isNotEmpty(model.getSessionEntries()), + var map = new LinkedHashMap, ObservableValue>(); + map.put(Comp.hspacer().styleClass("top-spacer"), new SimpleBooleanProperty(true)); + map.put(Comp.of(() -> createTabPane()), Bindings.isNotEmpty(model.getSessionEntries())); + map.put( new BrowserWelcomeComp(model).apply(struc -> StackPane.setAlignment(struc.get(), Pos.CENTER_LEFT)), Bindings.createBooleanBinding( () -> { return model.getSessionEntries().size() == 0; }, - model.getSessionEntries()), - Comp.hspacer().styleClass("top-spacer"), - new SimpleBooleanProperty(true))); + model.getSessionEntries())); + var multi = new MultiContentComp(map); multi.apply(struc -> ((StackPane) struc.get()).setAlignment(Pos.TOP_CENTER)); return multi.createRegion(); } @@ -198,28 +193,132 @@ public class BrowserSessionTabsComp extends SimpleComp { } }); - InputHelper.onInput(tabs, true, keyEvent -> { + tabs.addEventHandler(KeyEvent.KEY_PRESSED, keyEvent -> { var current = tabs.getSelectionModel().getSelectedItem(); if (current == null) { return; } - if (keyEvent.getCode() == KeyCode.W && keyEvent.isShortcutDown()) { + if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(keyEvent)) { tabs.getTabs().remove(current); keyEvent.consume(); + return; + } + + if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN).match(keyEvent)) { + tabs.getTabs().clear(); + keyEvent.consume(); } - if (keyEvent.getCode() == KeyCode.W && keyEvent.isShortcutDown() && keyEvent.isShiftDown()) { - tabs.getTabs().clear(); + if (keyEvent.getCode().isFunctionKey()) { + var start = KeyCode.F1.getCode(); + var index = keyEvent.getCode().getCode() - start; + if (index < tabs.getTabs().size()) { + tabs.getSelectionModel().select(index); + keyEvent.consume(); + return; + } + } + + var forward = new KeyCodeCombination(KeyCode.TAB, KeyCombination.SHORTCUT_DOWN); + if (forward.match(keyEvent)) { + var next = (tabs.getSelectionModel().getSelectedIndex() + 1) + % tabs.getTabs().size(); + tabs.getSelectionModel().select(next); keyEvent.consume(); + return; + } + + var back = new KeyCodeCombination(KeyCode.TAB, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN); + if (back.match(keyEvent)) { + var previous = (tabs.getTabs().size() + tabs.getSelectionModel().getSelectedIndex() - 1) + % tabs.getTabs().size(); + tabs.getSelectionModel().select(previous); + keyEvent.consume(); + return; } }); return tabs; } + private ContextMenu createContextMenu(TabPane tabs, Tab tab) { + var cm = ContextMenuHelper.create(); + + var select = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("selectTab")); + select.acceleratorProperty() + .bind(Bindings.createObjectBinding( + () -> { + var start = KeyCode.F1.getCode(); + var index = tabs.getTabs().indexOf(tab); + var keyCode = Arrays.stream(KeyCode.values()) + .filter(code -> code.getCode() == start + index) + .findAny() + .orElse(null); + return keyCode != null ? new KeyCodeCombination(keyCode) : null; + }, + tabs.getTabs())); + select.setOnAction(event -> { + tabs.getSelectionModel().select(tab); + event.consume(); + }); + cm.getItems().add(select); + + cm.getItems().add(new SeparatorMenuItem()); + + var close = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeTab")); + close.setAccelerator(new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN)); + close.setOnAction(event -> { + tabs.getTabs().remove(tab); + event.consume(); + }); + cm.getItems().add(close); + + var closeOthers = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeOtherTabs")); + closeOthers.setOnAction(event -> { + tabs.getTabs() + .removeAll(tabs.getTabs().stream().filter(t -> t != tab).toList()); + event.consume(); + }); + cm.getItems().add(closeOthers); + + var closeLeft = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeLeftTabs")); + closeLeft.setOnAction(event -> { + var index = tabs.getTabs().indexOf(tab); + tabs.getTabs() + .removeAll(tabs.getTabs().stream() + .filter(t -> tabs.getTabs().indexOf(t) < index) + .toList()); + event.consume(); + }); + cm.getItems().add(closeLeft); + + var closeRight = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeRightTabs")); + closeRight.setOnAction(event -> { + var index = tabs.getTabs().indexOf(tab); + tabs.getTabs() + .removeAll(tabs.getTabs().stream() + .filter(t -> tabs.getTabs().indexOf(t) > index) + .toList()); + event.consume(); + }); + cm.getItems().add(closeRight); + + var closeAll = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeAllTabs")); + closeAll.setAccelerator( + new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN)); + closeAll.setOnAction(event -> { + tabs.getTabs().clear(); + event.consume(); + }); + cm.getItems().add(closeAll); + + return cm; + } + private Tab createTab(TabPane tabs, BrowserSessionTab model) { var tab = new Tab(); + tab.setContextMenu(createContextMenu(tabs, tab)); var ring = new RingProgressIndicator(0, false); ring.setMinSize(16, 16); 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 8c3529643..32c419d36 100644 --- a/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java +++ b/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java @@ -13,10 +13,9 @@ import io.xpipe.app.storage.DataStorage; import javafx.beans.binding.Bindings; import javafx.beans.value.ObservableValue; +import javafx.scene.Parent; import javafx.scene.control.ButtonBase; -import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; -import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Pane; @@ -63,28 +62,11 @@ public class AppLayoutComp extends Comp> { sidebarR.getChildrenUnmodifiable().forEach(node -> { var shortcut = (KeyCodeCombination) node.getProperties().get("shortcut"); if (shortcut != null && shortcut.match(event)) { - ((ButtonBase) node).fire(); + ((ButtonBase) ((Parent) node).getChildrenUnmodifiable().get(1)).fire(); event.consume(); return; } }); - if (event.isConsumed()) { - return; - } - - var forward = new KeyCodeCombination(KeyCode.TAB, KeyCombination.CONTROL_DOWN); - if (forward.match(event)) { - var next = (model.getEntries().indexOf(model.getSelected().getValue()) + 1) % 3; - model.getSelected().setValue(model.getEntries().get(next)); - return; - } - - var back = new KeyCodeCombination(KeyCode.TAB, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN); - if (back.match(event)) { - var next = (model.getEntries().indexOf(model.getSelected().getValue()) + 2) % 3; - model.getSelected().setValue(model.getEntries().get(next)); - return; - } }); AppFont.normal(pane); pane.getStyleClass().add("layout"); 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 18ae445ca..cc4921cac 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 @@ -70,7 +70,9 @@ public class ListBoxViewComp extends Comp> { .bind(Bindings.createDoubleBinding( () -> { var v = bar.getVisibleAmount(); - return v < 1.0 ? 1.0 : 0.0; + // Check for rounding and accuracy issues + // It might not be exactly equal to 1.0 + return v < 0.99 ? 1.0 : 0.0; }, bar.visibleAmountProperty())); } 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 c0c5f70ed..6000be622 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 @@ -88,6 +88,7 @@ public class ListSelectorComp extends SimpleComp { var sp = new ScrollPane(vbox); sp.setFitToWidth(true); + sp.getStyleClass().add("list-selector-comp"); return sp; } } 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 6f43963ca..b977727df 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 @@ -22,6 +22,7 @@ import javafx.scene.web.WebEngine; import javafx.scene.web.WebView; import lombok.SneakyThrows; +import org.apache.commons.io.FileUtils; import java.io.IOException; import java.nio.file.Files; @@ -63,7 +64,7 @@ public class MarkdownComp extends Comp> { var html = MarkdownHelper.toHtml(markdown, s -> s, htmlTransformation, null); try { // Workaround for https://bugs.openjdk.org/browse/JDK-8199014 - Files.createDirectories(file.getParent()); + FileUtils.forceMkdir(file.getParent().toFile()); Files.writeString(file, html); return file; } catch (IOException e) { 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 0c26d5b09..2eb671cfc 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 @@ -11,6 +11,7 @@ import io.xpipe.app.fxcomps.impl.TooltipAugment; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.update.UpdateAvailableAlert; import io.xpipe.app.update.XPipeDistributionType; + import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.Property; @@ -41,14 +42,14 @@ public class SideMenuBarComp extends Comp> { var selectedBorder = Bindings.createObjectBinding( () -> { var c = Platform.getPreferences().getAccentColor().desaturate(); - return new Background(new BackgroundFill(c,new CornerRadii(8), new Insets(10, 1, 10, 2))); + return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(10, 1, 10, 2))); }, Platform.getPreferences().accentColorProperty()); var hoverBorder = Bindings.createObjectBinding( () -> { var c = Platform.getPreferences().getAccentColor().darker().desaturate(); - return new Background(new BackgroundFill(c,new CornerRadii(8), new Insets(10, 1, 10, 2))); + return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(10, 1, 10, 2))); }, Platform.getPreferences().accentColorProperty()); @@ -70,12 +71,9 @@ public class SideMenuBarComp extends Comp> { value.setValue(e); }); var shortcut = e.combination(); - if (shortcut != null) { - b.apply(struc -> struc.get().getProperties().put("shortcut", shortcut)); - } b.apply(new TooltipAugment<>(e.name(), shortcut)); b.apply(struc -> { - AppFont.setSize(struc.get(), 2); + AppFont.setSize(struc.get(), 1); struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e)); value.addListener((c, o, n) -> { PlatformThread.runLaterIfNeeded(() -> { @@ -86,7 +84,8 @@ public class SideMenuBarComp extends Comp> { b.accessibleText(e.name()); var indicator = Comp.empty().styleClass("indicator"); - var stack = new StackComp(List.of(indicator, b)).apply(struc -> struc.get().setAlignment(Pos.CENTER_RIGHT)); + var stack = new StackComp(List.of(indicator, b)) + .apply(struc -> struc.get().setAlignment(Pos.CENTER_RIGHT)); stack.apply(struc -> { var indicatorRegion = (Region) struc.get().getChildren().getFirst(); indicatorRegion.setMaxWidth(7); @@ -110,6 +109,9 @@ public class SideMenuBarComp extends Comp> { selectedBorder, noneBorder)); }); + if (shortcut != null) { + stack.apply(struc -> struc.get().getProperties().put("shortcut", shortcut)); + } vbox.getChildren().add(stack.createRegion()); } @@ -118,7 +120,7 @@ public class SideMenuBarComp extends Comp> { .tooltipKey("updateAvailableTooltip") .accessibleTextKey("updateAvailableTooltip"); b.apply(struc -> { - AppFont.setSize(struc.get(), 2); + AppFont.setSize(struc.get(), 1); }); b.hide(PlatformThread.sync(Bindings.createBooleanBinding( () -> { 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 502336500..06f42004a 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 @@ -95,7 +95,7 @@ public class StoreToggleComp extends SimpleComp { v -> { Platform.runLater(() -> { setter.accept(section.getWrapper().getEntry().getStore().asNeeded(), v); - StoreViewState.get().toggleStoreListUpdate(); + StoreViewState.get().triggerStoreListUpdate(); }); }); t.tooltipKey("showAllChildren"); 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 c9d0cabb2..1a1cc2640 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 @@ -33,16 +33,27 @@ public class SystemStateComp extends SimpleComp { PlatformThread.runLaterIfNeeded(() -> fi.setIconLiteral(i)); }); - var border = new FontIcon("mdi2c-circle-outline"); + var border = new FontIcon("mdi2s-square-rounded-outline"); border.getStyleClass().add("outer-icon"); - border.setOpacity(0.5); + border.setOpacity(0.3); var success = Styles.toDataURI( - ".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-success-emphasis; }"); + """ + .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; }"); + 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; }"); + Styles.toDataURI( + """ + .stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-accent-emphasis; } + """ + ); var pane = new StackedFontIcon(); pane.getChildren().addAll(fi, border); @@ -51,7 +62,7 @@ public class SystemStateComp extends SimpleComp { var dataClass1 = """ .stacked-ikonli-font-icon > .outer-icon { - -fx-icon-size: 22px; + -fx-icon-size: 26px; } .stacked-ikonli-font-icon > .inner-icon { -fx-icon-size: 12px; 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 a6c5db926..8ace6c03c 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 @@ -1,10 +1,8 @@ package io.xpipe.app.comp.store; -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 javafx.beans.binding.Bindings; import javafx.beans.property.SimpleStringProperty; import javafx.geometry.HPos; @@ -17,8 +15,8 @@ public class DenseStoreEntryComp extends StoreEntryComp { private final boolean showIcon; - public DenseStoreEntryComp(StoreEntryWrapper entry, boolean showIcon, Comp content) { - super(entry, content); + public DenseStoreEntryComp(StoreSection section, boolean showIcon, Comp content) { + super(section, content); this.showIcon = showIcon; } @@ -26,24 +24,27 @@ public class DenseStoreEntryComp extends StoreEntryComp { var information = new Label(); information.setGraphicTextGap(7); information.getStyleClass().add("information"); - AppFont.header(information); - var state = wrapper.getEntry().getProvider() != null - ? wrapper.getEntry().getProvider().stateDisplay(wrapper) + var state = getWrapper().getEntry().getProvider() != null + ? getWrapper().getEntry().getProvider().stateDisplay(getWrapper()) : Comp.empty(); information.setGraphic(state.createRegion()); - var info = wrapper.getEntry().getProvider() != null ? wrapper.getEntry().getProvider().informationString(wrapper) : new SimpleStringProperty(); - var summary = wrapper.getSummary(); - if (wrapper.getEntry().getProvider() != null) { + var info = getWrapper().getEntry().getProvider() != null + ? getWrapper().getEntry().getProvider().informationString(section) + : new SimpleStringProperty(); + var summary = getWrapper().getSummary(); + if (getWrapper().getEntry().getProvider() != null) { information .textProperty() .bind(PlatformThread.sync(Bindings.createStringBinding( () -> { var val = summary.getValue(); - if (val != null - && grid.isHover() - && wrapper.getEntry().getProvider().alwaysShowSummary()) { + var p = getWrapper().getEntry().getProvider(); + if (val != null && grid.isHover() + && p.alwaysShowSummary()) { + return val; + } else if (info.getValue() == null && p.alwaysShowSummary()){ return val; } else { return info.getValue(); @@ -73,11 +74,11 @@ public class DenseStoreEntryComp extends StoreEntryComp { return grid.getWidth() / 2.5; }, grid.widthProperty())); - var notes = new StoreNotesComp(wrapper).createRegion(); + var notes = new StoreNotesComp(getWrapper()).createRegion(); if (showIcon) { - var storeIcon = createIcon(30, 24); - grid.getColumnConstraints().add(new ColumnConstraints(46)); + var storeIcon = createIcon(28, 24); + grid.getColumnConstraints().add(new ColumnConstraints(38)); grid.add(storeIcon, 0, 0); GridPane.setHalignment(storeIcon, HPos.CENTER); } @@ -95,7 +96,7 @@ public class DenseStoreEntryComp extends StoreEntryComp { nameCC.setHgrow(Priority.ALWAYS); grid.getColumnConstraints().addAll(nameCC); var nameBox = new HBox(name, notes); - nameBox.setSpacing(1); + nameBox.setSpacing(6); nameBox.setAlignment(Pos.CENTER_LEFT); grid.addRow(0, nameBox); 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 595e7bc56..4d2914f23 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 @@ -1,16 +1,19 @@ package io.xpipe.app.comp.store; +import io.xpipe.app.core.AppFont; import io.xpipe.app.fxcomps.Comp; +import io.xpipe.core.process.OsType; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Pos; +import javafx.scene.control.Label; import javafx.scene.layout.*; public class StandardStoreEntryComp extends StoreEntryComp { - public StandardStoreEntryComp(StoreEntryWrapper entry, Comp content) { - super(entry, content); + public StandardStoreEntryComp(StoreSection section, Comp content) { + super(section, content); } @Override @@ -18,23 +21,37 @@ public class StandardStoreEntryComp extends StoreEntryComp { return true; } + private Label createSummary() { + var summary = new Label(); + summary.textProperty().bind(getWrapper().getSummary()); + summary.getStyleClass().add("summary"); + AppFont.small(summary); + return summary; + } + protected Region createContent() { var name = createName().createRegion(); - var notes = new StoreNotesComp(wrapper).createRegion(); + var notes = new StoreNotesComp(getWrapper()).createRegion(); var grid = new GridPane(); - grid.setHgap(7); - grid.setVgap(0); + grid.setHgap(6); + grid.setVgap(OsType.getLocal() == OsType.MACOS ? 2 : 0); - var storeIcon = createIcon(50, 40); + var storeIcon = createIcon(46, 40); grid.add(storeIcon, 0, 0, 1, 2); - grid.getColumnConstraints().add(new ColumnConstraints(66)); + grid.getColumnConstraints().add(new ColumnConstraints(56)); var nameAndNotes = new HBox(name, notes); - nameAndNotes.setSpacing(1); + nameAndNotes.setSpacing(6); nameAndNotes.setAlignment(Pos.CENTER_LEFT); grid.add(nameAndNotes, 1, 0); - grid.add(createSummary(), 1, 1); + GridPane.setVgrow(nameAndNotes, Priority.ALWAYS); + + var summaryBox = new HBox(createSummary()); + summaryBox.setAlignment(Pos.TOP_LEFT); + GridPane.setVgrow(summaryBox, Priority.ALWAYS); + grid.add(summaryBox, 1, 1); + var nameCC = new ColumnConstraints(); nameCC.setMinWidth(100); nameCC.setHgrow(Priority.ALWAYS); 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 c5ad100c6..414e8ac48 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 @@ -112,6 +112,11 @@ public class StoreCategoryWrapper { } public void update() { + // We are probably in shutdown then + if (StoreViewState.get() == null) { + return; + } + // Avoid reupdating name when changed from the name property! var catName = translatedName(category.getName()); if (!catName.equals(name.getValue())) { 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 ffcff4718..08e3458ad 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,5 +1,6 @@ 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; @@ -21,7 +22,6 @@ import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.util.*; import io.xpipe.core.store.DataStore; import io.xpipe.core.util.ValidationException; - import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.*; @@ -33,8 +33,6 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.stage.Stage; - -import atlantafx.base.controls.Spacer; import lombok.AccessLevel; import lombok.experimental.FieldDefaults; import net.synedra.validatorfx.GraphicDecorationStackPane; @@ -51,7 +49,7 @@ public class StoreCreationComp extends DialogComp { Stage window; BiConsumer consumer; Property provider; - Property store; + ObjectProperty store; Predicate filter; BooleanProperty busy = new SimpleBooleanProperty(); Property validator = new SimpleObjectProperty<>(new SimpleValidator()); @@ -60,6 +58,7 @@ public class StoreCreationComp extends DialogComp { ObservableValue entry; BooleanProperty changedSinceError = new SimpleBooleanProperty(); BooleanProperty skippable = new SimpleBooleanProperty(); + BooleanProperty connectable = new SimpleBooleanProperty(); StringProperty name; DataStoreEntry existingEntry; boolean staticDisplay; @@ -68,7 +67,7 @@ public class StoreCreationComp extends DialogComp { Stage window, BiConsumer consumer, Property provider, - Property store, + ObjectProperty store, Predicate filter, String initialName, DataStoreEntry existingEntry, @@ -96,6 +95,12 @@ public class StoreCreationComp extends DialogComp { } }); + this.provider.subscribe((n) -> { + if (n != null) { + connectable.setValue(n.canConnectDuringCreation()); + } + }); + this.apply(r -> { r.get().setPrefWidth(650); r.get().setPrefHeight(750); @@ -163,7 +168,12 @@ public class StoreCreationComp extends DialogComp { if (!DataStorage.get().getStoreEntries().contains(e)) { DataStorage.get().addStoreEntryIfNotPresent(newE); } else { - DataStorage.get().updateEntry(e, newE); + // We didn't change anything + if (e.getStore().equals(newE.getStore())) { + e.setName(newE.getName()); + } else { + DataStorage.get().updateEntry(e, newE); + } } }); }, @@ -239,7 +249,16 @@ public class StoreCreationComp extends DialogComp { finish(); } }) - .visible(skippable)); + .visible(skippable), + new ButtonComp(AppI18n.observable("connect"), null, () -> { + var temp = DataStoreEntry.createTempWrapper(store.getValue()); + var action = provider.getValue().launchAction(temp); + ThreadHelper.runFailableAsync(() -> { + action.execute(); + }); + }).hide(connectable.not().or(Bindings.createBooleanBinding(() -> { + return store.getValue() == null || !store.getValue().isComplete(); + }, store)))); } @Override @@ -393,11 +412,10 @@ public class StoreCreationComp extends DialogComp { private Region createLayout() { var layout = new BorderPane(); layout.getStyleClass().add("store-creator"); - layout.setPadding(new Insets(20)); var providerChoice = new StoreProviderChoiceComp(filter, provider, staticDisplay); - if (staticDisplay) { - providerChoice.apply(struc -> struc.get().setDisable(true)); - } else { + var showProviders = (!staticDisplay && (providerChoice.getProviders().size() > 1 || providerChoice.getProviders().getFirst().showProviderChoice())) || + (staticDisplay && provider.getValue().showProviderChoice()); + if (showProviders) { providerChoice.onSceneAssign(struc -> struc.get().requestFocus()); } providerChoice.apply(GrowAugment.create(true, false)); @@ -422,9 +440,14 @@ public class StoreCreationComp extends DialogComp { var sep = new Separator(); sep.getStyleClass().add("spacer"); - var top = new VBox(providerChoice.createRegion(), new Spacer(7, Orientation.VERTICAL), sep); + var top = new VBox(providerChoice.createRegion(), new Spacer(5, Orientation.VERTICAL), sep); top.getStyleClass().add("top"); - layout.setTop(top); + if (showProviders) { + layout.setTop(top); + layout.setPadding(new Insets(15, 20, 20, 20)); + } else { + layout.setPadding(new Insets(5, 20, 20, 20)); + } var valSp = new GraphicDecorationStackPane(); valSp.getChildren().add(layout); 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 b30ec5bb4..036d6cb9b 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 @@ -40,8 +40,9 @@ public class StoreCreationMenu { menu.getItems() .add(category("addTunnel", "mdi2v-vector-polyline-plus", DataStoreCreationCategory.TUNNEL, null)); - menu.getItems() - .add(category("addCommand", "mdi2c-code-greater-than", DataStoreCreationCategory.COMMAND, "cmd")); + menu.getItems().add(category("addCommand", "mdi2c-code-greater-than", DataStoreCreationCategory.COMMAND, null)); + + menu.getItems().add(category("addSerial", "mdi2s-serial-port", DataStoreCreationCategory.SERIAL, "serial")); menu.getItems().add(category("addDatabase", "mdi2d-database-plus", DataStoreCreationCategory.DATABASE, null)); } 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 045d05579..8f7aadbb5 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 @@ -14,6 +14,7 @@ import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.TooltipAugment; import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.DerivedObservableList; +import io.xpipe.app.fxcomps.util.LabelGraphic; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; @@ -23,6 +24,7 @@ import io.xpipe.app.update.XPipeDistributionType; import io.xpipe.app.util.*; import javafx.beans.binding.Bindings; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableDoubleValue; import javafx.css.PseudoClass; @@ -51,21 +53,25 @@ public abstract class StoreEntryComp extends SimpleComp { App.getApp().getStage().widthProperty().divide(2.1).add(-100); public static final ObservableDoubleValue INFO_WITH_CONTENT_WIDTH = App.getApp().getStage().widthProperty().divide(2.1).add(-200); - protected final StoreEntryWrapper wrapper; + protected final StoreSection section; protected final Comp content; - public StoreEntryComp(StoreEntryWrapper wrapper, Comp content) { - this.wrapper = wrapper; + public StoreEntryComp(StoreSection section, Comp content) { + this.section = section; this.content = content; } - public static StoreEntryComp create(StoreEntryWrapper entry, Comp content, boolean preferLarge) { + public StoreEntryWrapper getWrapper() { + return section.getWrapper(); + } + + public static StoreEntryComp create(StoreSection section, Comp content, boolean preferLarge) { var forceCondensed = AppPrefs.get() != null && AppPrefs.get().condenseConnectionDisplay().get(); if (!preferLarge || forceCondensed) { - return new DenseStoreEntryComp(entry, true, content); + return new DenseStoreEntryComp(section, true, content); } else { - return new StandardStoreEntryComp(entry, content); + return new StandardStoreEntryComp(section, content); } } @@ -76,9 +82,7 @@ public abstract class StoreEntryComp extends SimpleComp { } else { var forceCondensed = AppPrefs.get() != null && AppPrefs.get().condenseConnectionDisplay().get(); - return forceCondensed - ? new DenseStoreEntryComp(e.getWrapper(), true, null) - : new StandardStoreEntryComp(e.getWrapper(), null); + return forceCondensed ? new DenseStoreEntryComp(e, true, null) : new StandardStoreEntryComp(e, null); } } @@ -95,16 +99,16 @@ public abstract class StoreEntryComp extends SimpleComp { button.setPadding(Insets.EMPTY); button.setMaxWidth(5000); button.setFocusTraversable(true); - button.accessibleTextProperty().bind(wrapper.nameProperty()); + button.accessibleTextProperty().bind(getWrapper().nameProperty()); button.setOnAction(event -> { event.consume(); ThreadHelper.runFailableAsync(() -> { - wrapper.executeDefaultAction(); + getWrapper().executeDefaultAction(); }); }); button.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { if (AppPrefs.get().requireDoubleClickForConnections().get()) { - if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() > 2) { + if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() != 2) { event.consume(); } } else { @@ -115,7 +119,7 @@ public abstract class StoreEntryComp extends SimpleComp { }); button.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> { if (AppPrefs.get().requireDoubleClickForConnections().get()) { - if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() > 2) { + if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() != 2) { event.consume(); } } else { @@ -132,9 +136,12 @@ public abstract class StoreEntryComp extends SimpleComp { var loading = LoadingOverlayComp.noProgress( Comp.of(() -> button), - wrapper.getEntry().getValidity().isUsable() - ? wrapper.getBusy().or(wrapper.getEntry().getProvider().busy(wrapper)) - : wrapper.getBusy()); + getWrapper().getEntry().getValidity().isUsable() + ? getWrapper() + .getBusy() + .or(getWrapper().getEntry().getProvider().busy(getWrapper())) + : getWrapper().getBusy()); + AppFont.normal(button); return loading.createRegion(); } @@ -146,31 +153,22 @@ public abstract class StoreEntryComp extends SimpleComp { information .textProperty() .bind( - wrapper.getEntry().getProvider() != null + getWrapper().getEntry().getProvider() != null ? PlatformThread.sync( - wrapper.getEntry().getProvider().informationString(wrapper)) + getWrapper().getEntry().getProvider().informationString(section)) : new SimpleStringProperty()); information.getStyleClass().add("information"); - AppFont.header(information); - var state = wrapper.getEntry().getProvider() != null - ? wrapper.getEntry().getProvider().stateDisplay(wrapper) + var state = getWrapper().getEntry().getProvider() != null + ? getWrapper().getEntry().getProvider().stateDisplay(getWrapper()) : Comp.empty(); information.setGraphic(state.createRegion()); return information; } - protected Label createSummary() { - var summary = new Label(); - summary.textProperty().bind(wrapper.getSummary()); - summary.getStyleClass().add("summary"); - AppFont.small(summary); - return summary; - } - protected void applyState(Node node) { - PlatformThread.sync(wrapper.getValidity()).subscribe(val -> { + PlatformThread.sync(getWrapper().getValidity()).subscribe(val -> { switch (val) { case LOAD_FAILED -> { node.pseudoClassStateChanged(FAILED, true); @@ -189,24 +187,23 @@ public abstract class StoreEntryComp extends SimpleComp { } protected Comp createName() { - LabelComp name = new LabelComp(wrapper.nameProperty()); - name.apply(struc -> struc.get().setTextOverrun(OverrunStyle.CENTER_ELLIPSIS)) - .apply(struc -> struc.get().setPadding(new Insets(5, 5, 5, 0))); - name.apply(s -> AppFont.header(s.get())); + LabelComp name = new LabelComp(getWrapper().nameProperty()); + name.apply(struc -> struc.get().setTextOverrun(OverrunStyle.CENTER_ELLIPSIS)); name.styleClass("name"); return name; } protected Node createIcon(int w, int h) { - var img = wrapper.disabledProperty().get() + var img = getWrapper().disabledProperty().get() ? "disabled_icon.png" - : wrapper.getEntry() + : getWrapper() + .getEntry() .getProvider() - .getDisplayIconFileName(wrapper.getEntry().getStore()); + .getDisplayIconFileName(getWrapper().getEntry().getStore()); var imageComp = PrettyImageHelper.ofFixedSize(img, w, h); var storeIcon = imageComp.createRegion(); - if (wrapper.getValidity().getValue().isUsable()) { - new TooltipAugment<>(wrapper.getEntry().getProvider().displayName(), null).augment(storeIcon); + if (getWrapper().getValidity().getValue().isUsable()) { + new TooltipAugment<>(getWrapper().getEntry().getProvider().displayName(), null).augment(storeIcon); } var stack = new StackPane(storeIcon); @@ -220,7 +217,7 @@ public abstract class StoreEntryComp extends SimpleComp { } protected Region createButtonBar() { - var list = new DerivedObservableList<>(wrapper.getActionProviders(), false); + var list = new DerivedObservableList<>(getWrapper().getActionProviders(), false); var buttons = list.mapped(actionProvider -> { var button = buildButton(actionProvider); return button != null ? button.createRegion() : null; @@ -239,8 +236,8 @@ public abstract class StoreEntryComp extends SimpleComp { buttons.subscribe(update); update.run(); ig.setAlignment(Pos.CENTER_RIGHT); - ig.setPadding(new Insets(5)); ig.getStyleClass().add("button-bar"); + AppFont.medium(ig); return ig; } @@ -249,17 +246,20 @@ public abstract class StoreEntryComp extends SimpleComp { var branch = p.getBranchDataStoreCallSite(); var cs = leaf != null ? leaf : branch; - if (cs == null || !cs.isMajor(wrapper.getEntry().ref())) { + if (cs == null || !cs.isMajor(getWrapper().getEntry().ref())) { return null; } + var icon = new SimpleObjectProperty<>(new LabelGraphic.IconGraphic(cs.getIcon(getWrapper().getEntry().ref()))); var button = new IconButtonComp( - cs.getIcon(wrapper.getEntry().ref()), + icon, leaf != null ? () -> { ThreadHelper.runFailableAsync(() -> { - wrapper.runAction( - leaf.createAction(wrapper.getEntry().ref()), leaf.showBusy()); + getWrapper().runAction( + leaf.createAction(getWrapper().getEntry().ref()), leaf.showBusy()); + // Update icon in case action changed it + icon.set(new LabelGraphic.IconGraphic(cs.getIcon(getWrapper().getEntry().ref()))); }); } : null); @@ -267,7 +267,7 @@ public abstract class StoreEntryComp extends SimpleComp { button.apply(new ContextMenuAugment<>( mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY, keyEvent -> false, () -> { var cm = ContextMenuHelper.create(); - branch.getChildren().forEach(childProvider -> { + branch.getChildren(getWrapper().getEntry().ref()).forEach(childProvider -> { var menu = buildMenuItemForAction(childProvider); if (menu != null) { cm.getItems().add(menu); @@ -276,13 +276,13 @@ public abstract class StoreEntryComp extends SimpleComp { return cm; })); } - button.accessibleText(cs.getName(wrapper.getEntry().ref()).getValue()); - button.apply(new TooltipAugment<>(cs.getName(wrapper.getEntry().ref()), null)); + button.accessibleText(cs.getName(getWrapper().getEntry().ref()).getValue()); + button.apply(new TooltipAugment<>(cs.getName(getWrapper().getEntry().ref()), null)); return button; } protected Comp createSettingsButton() { - var settingsButton = new IconButtonComp("mdi2d-dots-horizontal-circle-outline", null); + var settingsButton = new IconButtonComp("mdi2d-dots-horizontal-circle-outline"); settingsButton.styleClass("settings"); settingsButton.accessibleText("More"); settingsButton.apply(new ContextMenuAugment<>( @@ -298,7 +298,7 @@ public abstract class StoreEntryComp extends SimpleComp { AppFont.normal(contextMenu.getStyleableNode()); var hasSep = false; - for (var p : wrapper.getActionProviders()) { + for (var p : getWrapper().getActionProviders()) { var item = buildMenuItemForAction(p); if (item == null) { continue; @@ -321,36 +321,36 @@ public abstract class StoreEntryComp extends SimpleComp { var notes = new MenuItem(AppI18n.get("addNotes"), new FontIcon("mdi2n-note-text")); notes.setOnAction(event -> { - wrapper.getNotes().setValue(new StoreNotes(null, getDefaultNotes())); + getWrapper().getNotes().setValue(new StoreNotes(null, getDefaultNotes())); event.consume(); }); - notes.visibleProperty().bind(BindingsHelper.map(wrapper.getNotes(), s -> s.getCommited() == null)); + notes.visibleProperty().bind(BindingsHelper.map(getWrapper().getNotes(), s -> s.getCommited() == null)); contextMenu.getItems().add(notes); if (AppPrefs.get().developerMode().getValue()) { var browse = new MenuItem(AppI18n.get("browseInternalStorage"), new FontIcon("mdi2f-folder-open-outline")); - browse.setOnAction( - event -> DesktopHelper.browsePathLocal(wrapper.getEntry().getDirectory())); + browse.setOnAction(event -> + DesktopHelper.browsePathLocal(getWrapper().getEntry().getDirectory())); contextMenu.getItems().add(browse); var copyId = new MenuItem(AppI18n.get("copyId"), new FontIcon("mdi2c-content-copy")); copyId.setOnAction(event -> - ClipboardHelper.copyText(wrapper.getEntry().getUuid().toString())); + ClipboardHelper.copyText(getWrapper().getEntry().getUuid().toString())); contextMenu.getItems().add(copyId); } - if (DataStorage.get().isRootEntry(wrapper.getEntry())) { + if (DataStorage.get().isRootEntry(getWrapper().getEntry())) { var color = new Menu(AppI18n.get("color"), new FontIcon("mdi2f-format-color-fill")); var none = new MenuItem("None"); none.setOnAction(event -> { - wrapper.getEntry().setColor(null); + getWrapper().getEntry().setColor(null); event.consume(); }); color.getItems().add(none); Arrays.stream(DataStoreColor.values()).forEach(dataStoreColor -> { MenuItem m = new MenuItem(DataStoreFormatter.capitalize(dataStoreColor.getId())); m.setOnAction(event -> { - wrapper.getEntry().setColor(dataStoreColor); + getWrapper().getEntry().setColor(dataStoreColor); event.consume(); }); color.getItems().add(m); @@ -358,10 +358,10 @@ public abstract class StoreEntryComp extends SimpleComp { contextMenu.getItems().add(color); } - if (wrapper.getEntry().getProvider() != null) { + if (getWrapper().getEntry().getProvider() != null) { var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline")); StoreViewState.get() - .getSortedCategories(wrapper.getCategory().getValue().getRoot()) + .getSortedCategories(getWrapper().getCategory().getValue().getRoot()) .getList() .forEach(storeCategoryWrapper -> { MenuItem m = new MenuItem(); @@ -369,12 +369,12 @@ public abstract class StoreEntryComp extends SimpleComp { .setValue(" ".repeat(storeCategoryWrapper.getDepth()) + storeCategoryWrapper.getName().getValue()); m.setOnAction(event -> { - wrapper.moveTo(storeCategoryWrapper.getCategory()); + getWrapper().moveTo(storeCategoryWrapper.getCategory()); event.consume(); }); if (storeCategoryWrapper.getParent() == null || storeCategoryWrapper.equals( - wrapper.getCategory().getValue())) { + getWrapper().getCategory().getValue())) { m.setDisable(true); } @@ -386,10 +386,10 @@ public abstract class StoreEntryComp extends SimpleComp { var order = new Menu(AppI18n.get("order"), new FontIcon("mdal-bookmarks")); var noOrder = new MenuItem(AppI18n.get("none"), new FontIcon("mdi2r-reorder-horizontal")); noOrder.setOnAction(event -> { - wrapper.setOrder(null); + getWrapper().setOrder(null); event.consume(); }); - if (wrapper.getEntry().getExplicitOrder() == null) { + if (getWrapper().getEntry().getExplicitOrder() == null) { noOrder.setDisable(true); } order.getItems().add(noOrder); @@ -397,20 +397,20 @@ public abstract class StoreEntryComp extends SimpleComp { var top = new MenuItem(AppI18n.get("stickToTop"), new FontIcon("mdi2o-order-bool-descending")); top.setOnAction(event -> { - wrapper.setOrder(DataStoreEntry.Order.TOP); + getWrapper().setOrder(DataStoreEntry.Order.TOP); event.consume(); }); - if (DataStoreEntry.Order.TOP.equals(wrapper.getEntry().getExplicitOrder())) { + if (DataStoreEntry.Order.TOP.equals(getWrapper().getEntry().getExplicitOrder())) { top.setDisable(true); } order.getItems().add(top); var bottom = new MenuItem(AppI18n.get("stickToBottom"), new FontIcon("mdi2o-order-bool-ascending")); bottom.setOnAction(event -> { - wrapper.setOrder(DataStoreEntry.Order.BOTTOM); + getWrapper().setOrder(DataStoreEntry.Order.BOTTOM); event.consume(); }); - if (DataStoreEntry.Order.BOTTOM.equals(wrapper.getEntry().getExplicitOrder())) { + if (DataStoreEntry.Order.BOTTOM.equals(getWrapper().getEntry().getExplicitOrder())) { bottom.setDisable(true); } order.getItems().add(bottom); @@ -423,14 +423,14 @@ public abstract class StoreEntryComp extends SimpleComp { del.disableProperty() .bind(Bindings.createBooleanBinding( () -> { - return !wrapper.getDeletable().get() + return !getWrapper().getDeletable().get() && !AppPrefs.get() .developerDisableGuiRestrictions() .get(); }, - wrapper.getDeletable(), + getWrapper().getDeletable(), AppPrefs.get().developerDisableGuiRestrictions())); - del.setOnAction(event -> wrapper.delete()); + del.setOnAction(event -> getWrapper().delete()); contextMenu.getItems().add(del); return contextMenu; @@ -441,12 +441,12 @@ public abstract class StoreEntryComp extends SimpleComp { var branch = p.getBranchDataStoreCallSite(); var cs = leaf != null ? leaf : branch; - if (cs == null || cs.isMajor(wrapper.getEntry().ref())) { + if (cs == null || cs.isMajor(getWrapper().getEntry().ref())) { return null; } - var name = cs.getName(wrapper.getEntry().ref()); - var icon = cs.getIcon(wrapper.getEntry().ref()); + var name = cs.getName(getWrapper().getEntry().ref()); + var icon = cs.getIcon(getWrapper().getEntry().ref()); var item = (leaf != null && leaf.canLinkTo()) || branch != null ? new Menu(null, new FontIcon(icon)) : new MenuItem(null, new FontIcon(icon)); @@ -462,7 +462,7 @@ public abstract class StoreEntryComp extends SimpleComp { Menu menu = item instanceof Menu m ? m : null; if (branch != null) { - var items = branch.getChildren().stream() + var items = branch.getChildren(getWrapper().getEntry().ref()).stream() .map(c -> buildMenuItemForAction(c)) .toList(); menu.getItems().addAll(items); @@ -472,21 +472,25 @@ public abstract class StoreEntryComp extends SimpleComp { run.textProperty().bind(AppI18n.observable("base.execute")); run.setOnAction(event -> { ThreadHelper.runFailableAsync(() -> { - wrapper.runAction(leaf.createAction(wrapper.getEntry().ref()), leaf.showBusy()); + getWrapper() + .runAction(leaf.createAction(getWrapper().getEntry().ref()), leaf.showBusy()); }); }); menu.getItems().add(run); var sc = new MenuItem(null, new FontIcon("mdi2c-code-greater-than")); - var url = "xpipe://action/" + p.getId() + "/" + wrapper.getEntry().getUuid(); + var url = "xpipe://action/" + p.getId() + "/" + + getWrapper().getEntry().getUuid(); sc.textProperty().bind(AppI18n.observable("base.createShortcut")); sc.setOnAction(event -> { ThreadHelper.runFailableAsync(() -> { - DesktopShortcuts.create( + DesktopShortcuts.createCliOpen( url, - wrapper.nameProperty().getValue() + " (" + DataStorage.get() + .getStoreEntryDisplayName( + getWrapper().getEntry()) + " (" + p.getLeafDataStoreCallSite() - .getName(wrapper.getEntry().ref()) + .getName(getWrapper().getEntry().ref()) .getValue() + ")"); }); }); @@ -516,7 +520,7 @@ public abstract class StoreEntryComp extends SimpleComp { event.consume(); ThreadHelper.runFailableAsync(() -> { - wrapper.runAction(leaf.createAction(wrapper.getEntry().ref()), leaf.showBusy()); + getWrapper().runAction(leaf.createAction(getWrapper().getEntry().ref()), leaf.showBusy()); }); }); diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListOverviewComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListOverviewComp.java index 4c7755431..60823dd6a 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListOverviewComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListOverviewComp.java @@ -8,6 +8,7 @@ import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.impl.FilterComp; import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.util.BindingsHelper; +import io.xpipe.app.fxcomps.util.LabelGraphic; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.process.OsType; @@ -95,8 +96,8 @@ public class StoreEntryListOverviewComp extends SimpleComp { createDateSortButton().createRegion(), Comp.hspacer(2).createRegion(), createAlphabeticalSortButton().createRegion()); - AppFont.setSize(label, 3); - AppFont.setSize(c, 3); + AppFont.setSize(label, 2); + AppFont.setSize(c, 2); topBar.setAlignment(Pos.CENTER); topBar.getStyleClass().add("top"); return topBar; @@ -111,9 +112,11 @@ public class StoreEntryListOverviewComp extends SimpleComp { }); var filter = new FilterComp(StoreViewState.get().getFilterString()); var f = filter.createRegion(); - var buttons = createAddButton(); - var hbox = new HBox(buttons, f); - f.prefHeightProperty().bind(buttons.heightProperty()); + var button = createAddButton(); + var hbox = new HBox(button, f); + f.minHeightProperty().bind(button.heightProperty()); + f.prefHeightProperty().bind(button.heightProperty()); + f.maxHeightProperty().bind(button.heightProperty()); hbox.setSpacing(8); hbox.setAlignment(Pos.CENTER); HBox.setHgrow(f, Priority.ALWAYS); @@ -136,22 +139,22 @@ public class StoreEntryListOverviewComp extends SimpleComp { if (OsType.getLocal().equals(OsType.MACOS)) { menu.setPadding(new Insets(-2, 0, -2, 0)); } else { - menu.setPadding(new Insets(-3, 0, -3, 0)); + menu.setPadding(new Insets(-4, 0, -4, 0)); } return menu; } private Comp createAlphabeticalSortButton() { - var icon = Bindings.createStringBinding( + var icon = Bindings.createObjectBinding( () -> { if (sortMode.getValue() == StoreSortMode.ALPHABETICAL_ASC) { - return "mdi2s-sort-alphabetical-descending"; + return new LabelGraphic.IconGraphic("mdi2s-sort-alphabetical-descending"); } if (sortMode.getValue() == StoreSortMode.ALPHABETICAL_DESC) { - return "mdi2s-sort-alphabetical-ascending"; + return new LabelGraphic.IconGraphic("mdi2s-sort-alphabetical-ascending"); } - return "mdi2s-sort-alphabetical-descending"; + return new LabelGraphic.IconGraphic("mdi2s-sort-alphabetical-descending"); }, sortMode); var alphabetical = new IconButtonComp(icon, () -> { @@ -164,6 +167,7 @@ public class StoreEntryListOverviewComp extends SimpleComp { } }); alphabetical.apply(alphabeticalR -> { + AppFont.medium(alphabeticalR.get()); alphabeticalR .get() .opacityProperty() @@ -183,15 +187,15 @@ public class StoreEntryListOverviewComp extends SimpleComp { } private Comp createDateSortButton() { - var icon = Bindings.createStringBinding( + var icon = Bindings.createObjectBinding( () -> { if (sortMode.getValue() == StoreSortMode.DATE_ASC) { - return "mdi2s-sort-clock-ascending-outline"; + return new LabelGraphic.IconGraphic("mdi2s-sort-clock-ascending-outline"); } if (sortMode.getValue() == StoreSortMode.DATE_DESC) { - return "mdi2s-sort-clock-descending-outline"; + return new LabelGraphic.IconGraphic("mdi2s-sort-clock-descending-outline"); } - return "mdi2s-sort-clock-ascending-outline"; + return new LabelGraphic.IconGraphic("mdi2s-sort-clock-ascending-outline"); }, sortMode); var date = new IconButtonComp(icon, () -> { @@ -204,6 +208,7 @@ public class StoreEntryListOverviewComp extends SimpleComp { } }); date.apply(dateR -> { + AppFont.medium(dateR.get()); dateR.get() .opacityProperty() .bind(Bindings.createDoubleBinding( 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 37b40b2f2..6111c4fdd 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 @@ -40,12 +40,13 @@ public class StoreEntryWrapper { private final Property category = new SimpleObjectProperty<>(); private final Property summary = new SimpleObjectProperty<>(); private final Property notes; + private final IntegerProperty childrenStateUpdateObservable = new SimpleIntegerProperty(); public StoreEntryWrapper(DataStoreEntry entry) { this.entry = entry; this.name = new SimpleStringProperty(entry.getName()); this.lastAccess = new SimpleObjectProperty<>(entry.getLastAccess().minus(Duration.ofMillis(500))); - ActionProvider.ALL.stream() + ActionProvider.ALL_STANDALONE.stream() .filter(dataStoreActionProvider -> { return !entry.isDisabled() && dataStoreActionProvider.getLeafDataStoreCallSite() != null @@ -63,6 +64,12 @@ public class StoreEntryWrapper { setupListeners(); } + public void triggerChildrenStateUpdate() { + PlatformThread.runLaterIfNeeded(() -> { + childrenStateUpdateObservable.set(childrenStateUpdateObservable.get() + 1); + }); + } + public void applyLastAccess() { this.lastAccessApplied.setValue(lastAccess.getValue()); } @@ -151,7 +158,8 @@ public class StoreEntryWrapper { summary.setValue(null); } else { try { - summary.setValue(entry.getProvider() != null ? entry.getProvider().summaryString(this) : null); + summary.setValue( + entry.getProvider() != null ? entry.getProvider().summaryString(this) : null); } catch (Exception ex) { // Summary creation might fail or have a bug ErrorEvent.fromThrowable(ex).handle(); @@ -163,18 +171,18 @@ public class StoreEntryWrapper { defaultActionProvider.setValue(null); } else { try { - var defaultProvider = ActionProvider.ALL.stream() + var defaultProvider = ActionProvider.ALL_STANDALONE.stream() .filter(e -> entry.getStore() != null && e.getDefaultDataStoreCallSite() != null && e.getDefaultDataStoreCallSite() - .getApplicableClass() - .isAssignableFrom(entry.getStore().getClass()) + .getApplicableClass() + .isAssignableFrom(entry.getStore().getClass()) && e.getDefaultDataStoreCallSite().isApplicable(entry.ref())) .findFirst() .orElse(null); this.defaultActionProvider.setValue(defaultProvider); - var newProviders = ActionProvider.ALL.stream() + var newProviders = ActionProvider.ALL_STANDALONE.stream() .filter(dataStoreActionProvider -> { return showActionProvider(dataStoreActionProvider); }) @@ -203,7 +211,7 @@ public class StoreEntryWrapper { if (branch != null && entry.getStore() != null && branch.getApplicableClass().isAssignableFrom(entry.getStore().getClass())) { - return branch.getChildren().stream().anyMatch(child -> { + return branch.getChildren(entry.ref()).stream().anyMatch(child -> { return showActionProvider(child); }); } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreNotesComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreNotesComp.java index 7b1328641..35431fb16 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreNotesComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreNotesComp.java @@ -39,7 +39,6 @@ public class StoreNotesComp extends Comp { .focusTraversableForAccessibility() .tooltipKey("notes") .styleClass("notes-button") - .grow(false, true) .hide(BindingsHelper.map(n, s -> s.getCommited() == null && s.getCurrent() == null)) .createStructure() .get(); 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 index d7b1e4d80..1833e589c 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreProviderChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreProviderChoiceComp.java @@ -29,7 +29,7 @@ public class StoreProviderChoiceComp extends Comp provider; boolean staticDisplay; - private List getProviders() { + public List getProviders() { return DataStoreProviders.getAll().stream() .filter(val -> filter == null || filter.test(val)) .toList(); diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreQuickAccessButtonComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreQuickAccessButtonComp.java index 57d39e0ef..47a513865 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreQuickAccessButtonComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreQuickAccessButtonComp.java @@ -4,6 +4,7 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.PrettyImageHelper; +import io.xpipe.app.fxcomps.util.LabelGraphic; import io.xpipe.app.util.ContextMenuHelper; import javafx.geometry.Side; @@ -18,9 +19,9 @@ import java.util.function.Consumer; public class StoreQuickAccessButtonComp extends Comp> { private final StoreSection section; - private final Consumer action; + private final Consumer action; - public StoreQuickAccessButtonComp(StoreSection section, Consumer action) { + public StoreQuickAccessButtonComp(StoreSection section, Consumer action) { this.section = section; this.action = action; } @@ -44,10 +45,9 @@ public class StoreQuickAccessButtonComp extends Comp> { w.getEntry().getProvider().getDisplayIconFileName(w.getEntry().getStore()); if (c.getList().isEmpty()) { var item = ContextMenuHelper.item( - PrettyImageHelper.ofFixedSizeSquare(graphic, 16), - w.getName().getValue()); + new LabelGraphic.ImageGraphic(graphic, 16), w.getName().getValue()); item.setOnAction(event -> { - action.accept(w); + action.accept(section); contextMenu.hide(); event.consume(); }); @@ -72,7 +72,7 @@ public class StoreQuickAccessButtonComp extends Comp> { return; } - action.accept(w); + action.accept(section); contextMenu.hide(); event.consume(); } 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 b2527bf91..1e7875f72 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 @@ -163,10 +163,10 @@ public class StoreSection { var allChildren = all.filtered( other -> { // 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); +// if (true) return DataStorage.get() +// .getDefaultDisplayParent(other.getEntry()) +// .map(found -> found.equals(e.getEntry())) +// .orElse(false); // is children. This check is fast as the children are cached in the storage return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry()) 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 6553b83c3..3085ff39b 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 @@ -7,6 +7,7 @@ import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.VerticalComp; +import io.xpipe.app.fxcomps.util.LabelGraphic; import io.xpipe.app.storage.DataStoreColor; import io.xpipe.app.util.ThreadHelper; @@ -44,9 +45,9 @@ public class StoreSectionComp extends Comp> { return section.getShownChildren().getList().isEmpty(); }, section.getShownChildren().getList()); - Consumer quickAccessAction = w -> { + Consumer quickAccessAction = w -> { ThreadHelper.runFailableAsync(() -> { - w.executeDefaultAction(); + w.getWrapper().executeDefaultAction(); }); }; var quickAccessButton = new StoreQuickAccessButtonComp(section, quickAccessAction) @@ -68,11 +69,11 @@ public class StoreSectionComp extends Comp> { private Comp> createExpandButton() { var expandButton = new IconButtonComp( - Bindings.createStringBinding( - () -> section.getWrapper().getExpanded().get() + Bindings.createObjectBinding( + () -> new LabelGraphic.IconGraphic(section.getWrapper().getExpanded().get() && section.getShownChildren().getList().size() > 0 ? "mdal-keyboard_arrow_down" - : "mdal-keyboard_arrow_right", + : "mdal-keyboard_arrow_right"), section.getWrapper().getExpanded(), section.getShownChildren().getList()), () -> { 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 58b56ecb1..b041622c2 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 @@ -8,6 +8,7 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.VerticalComp; +import io.xpipe.app.fxcomps.util.LabelGraphic; import io.xpipe.app.storage.DataStoreColor; import javafx.beans.binding.Bindings; @@ -34,12 +35,12 @@ public class StoreSectionMiniComp extends Comp> { private final StoreSection section; private final BiConsumer>> augment; - private final Consumer action; + private final Consumer action; public StoreSectionMiniComp( StoreSection section, BiConsumer>> augment, - Consumer action) { + Consumer action) { this.section = section; this.augment = augment; this.action = action; @@ -68,7 +69,7 @@ public class StoreSectionMiniComp extends Comp> { }) .apply(struc -> { struc.get().setOnAction(event -> { - action.accept(section.getWrapper()); + action.accept(section); event.consume(); }); }) @@ -81,8 +82,8 @@ public class StoreSectionMiniComp extends Comp> { new SimpleBooleanProperty(section.getWrapper().getExpanded().get() && section.getShownChildren().getList().size() > 0); var button = new IconButtonComp( - Bindings.createStringBinding( - () -> expanded.get() ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right", + Bindings.createObjectBinding( + () -> new LabelGraphic.IconGraphic(expanded.get() ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right"), expanded), () -> { expanded.set(!expanded.get()); @@ -105,7 +106,7 @@ public class StoreSectionMiniComp extends Comp> { return section.getShownChildren().getList().isEmpty(); }, section.getShownChildren().getList()); - Consumer quickAccessAction = action; + Consumer quickAccessAction = action; var quickAccessButton = new StoreQuickAccessButtonComp(section, quickAccessAction) .vgrow() .styleClass("quick-access-button") 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 132de45c0..4ea5cecc1 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 @@ -65,8 +65,7 @@ public interface StoreSortMode { .isUsable()) .map(this::representative), Stream.of(s)) - .max(Comparator.comparing( - section -> date(section))) + .max(Comparator.comparing(section -> date(section))) .orElseThrow(); } @@ -103,8 +102,7 @@ public interface StoreSortMode { .isUsable()) .map(this::representative), Stream.of(s)) - .max(Comparator.comparing( - section -> date(section))) + .max(Comparator.comparing(section -> date(section))) .orElseThrow(); } 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 69506d85b..b8ad517fa 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 @@ -124,10 +124,10 @@ public class StoreViewState { public void updateDisplay() { allEntries.getList().forEach(e -> e.applyLastAccess()); - toggleStoreListUpdate(); + triggerStoreListUpdate(); } - public void toggleStoreListUpdate() { + public void triggerStoreListUpdate() { PlatformThread.runLaterIfNeeded(() -> { entriesListUpdateObservable.set(entriesListUpdateObservable.get() + 1); }); @@ -152,7 +152,7 @@ public class StoreViewState { @Override public void onStoreListUpdate() { Platform.runLater(() -> { - toggleStoreListUpdate(); + triggerStoreListUpdate(); }); } 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 6abc9947d..c370764a2 100644 --- a/app/src/main/java/io/xpipe/app/core/App.java +++ b/app/src/main/java/io/xpipe/app/core/App.java @@ -7,9 +7,11 @@ import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.update.XPipeDistributionType; import io.xpipe.app.util.LicenseProvider; + import javafx.application.Application; import javafx.beans.binding.Bindings; import javafx.stage.Stage; + import lombok.Getter; import lombok.SneakyThrows; diff --git a/app/src/main/java/io/xpipe/app/core/AppDataLock.java b/app/src/main/java/io/xpipe/app/core/AppDataLock.java index 8e3f93154..d84202530 100644 --- a/app/src/main/java/io/xpipe/app/core/AppDataLock.java +++ b/app/src/main/java/io/xpipe/app/core/AppDataLock.java @@ -1,6 +1,7 @@ package io.xpipe.app.core; import io.xpipe.app.issue.ErrorEvent; +import org.apache.commons.io.FileUtils; import java.io.RandomAccessFile; import java.nio.channels.FileChannel; @@ -21,7 +22,7 @@ public class AppDataLock { public static boolean lock() { try { var file = getLockFile().toFile(); - Files.createDirectories(file.toPath().getParent()); + FileUtils.forceMkdir(file.getParentFile()); if (!Files.exists(file.toPath())) { try { // It is possible that another instance creates the lock at almost the same time 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 a3061cecf..50ca49aaf 100644 --- a/app/src/main/java/io/xpipe/app/core/AppExtensionManager.java +++ b/app/src/main/java/io/xpipe/app/core/AppExtensionManager.java @@ -93,7 +93,7 @@ public class AppExtensionManager { Path p = Path.of(localInstallation); if (!Files.exists(p)) { throw new IllegalStateException( - "Required local XPipe installation was not found but is required for development"); + "Required local XPipe installation was not found but is required for development. See https://github.com/xpipe-io/xpipe/blob/master/CONTRIBUTING.md#development-setup"); } var iv = getLocalInstallVersion(); @@ -105,8 +105,9 @@ public class AppExtensionManager { var sourceVersion = AppVersion.parse(sv) .orElseThrow(() -> new IllegalArgumentException("Invalid source version: " + sv)); if (AppProperties.get().isLocatorVersionCheck() && !installVersion.equals(sourceVersion)) { - throw new IllegalStateException("Incompatible development version. Source: " + iv + ", Installation: " - + sv + "\n\nPlease try to check out the matching release version in the repository."); + throw new IllegalStateException( + "Incompatible development version. Source: " + iv + ", Installation: " + sv + + "\n\nPlease try to check out the matching release version in the repository. See https://github.com/xpipe-io/xpipe/blob/master/CONTRIBUTING.md#development-setup"); } var extensions = XPipeInstallation.getLocalExtensionsDirectory(p); 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 1d94ed99d..b44ac8813 100644 --- a/app/src/main/java/io/xpipe/app/core/AppGreetings.java +++ b/app/src/main/java/io/xpipe/app/core/AppGreetings.java @@ -55,6 +55,12 @@ public class AppGreetings { if (set || AppProperties.get().isDevelopmentEnvironment()) { return; } + + if (AppProperties.get().isAutoAcceptEula()) { + AppCache.update("legalAccepted", true); + return; + } + var read = new SimpleBooleanProperty(); var accepted = new SimpleBooleanProperty(); AppWindowHelper.showBlockingAlert(alert -> { 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 ceb86ffd5..b58fb9e2e 100644 --- a/app/src/main/java/io/xpipe/app/core/AppLayoutModel.java +++ b/app/src/main/java/io/xpipe/app/core/AppLayoutModel.java @@ -78,19 +78,19 @@ public class AppLayoutModel { "mdi2f-file-cabinet", new BrowserSessionComp(BrowserSessionModel.DEFAULT), null, - new KeyCodeCombination(KeyCode.DIGIT1, KeyCombination.CONTROL_DOWN)), + new KeyCodeCombination(KeyCode.DIGIT1, KeyCombination.SHORTCUT_DOWN)), new Entry( AppI18n.observable("connections"), "mdi2c-connection", new StoreLayoutComp(), null, - new KeyCodeCombination(KeyCode.DIGIT2, KeyCombination.CONTROL_DOWN)), + new KeyCodeCombination(KeyCode.DIGIT2, KeyCombination.SHORTCUT_DOWN)), new Entry( AppI18n.observable("settings"), "mdsmz-miscellaneous_services", new AppPrefsComp(), null, - new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.CONTROL_DOWN)), + new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.SHORTCUT_DOWN)), new Entry( AppI18n.observable("explorePlans"), "mdi2p-professional-hexagon", @@ -102,20 +102,20 @@ public class AppLayoutModel { "mdi2g-github", null, () -> Hyperlinks.open(Hyperlinks.GITHUB), - new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.CONTROL_DOWN)), + null), new Entry( AppI18n.observable("discord"), "mdi2d-discord", null, () -> Hyperlinks.open(Hyperlinks.DISCORD), - new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.CONTROL_DOWN)), + null), new Entry( AppI18n.observable("api"), "mdi2c-code-json", null, () -> Hyperlinks.open( "http://localhost:" + AppBeaconServer.get().getPort()), - new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.CONTROL_DOWN)))); + null))); return l; } @@ -128,5 +128,6 @@ public class AppLayoutModel { double browserConnectionsWidth; } - public record Entry(ObservableValue name, String icon, Comp comp, Runnable action, KeyCombination combination) {} + public record Entry( + ObservableValue name, String icon, Comp comp, Runnable action, KeyCombination combination) {} } 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 efda9ad5f..b24d2190c 100644 --- a/app/src/main/java/io/xpipe/app/core/AppLogs.java +++ b/app/src/main/java/io/xpipe/app/core/AppLogs.java @@ -138,7 +138,7 @@ public class AppLogs { var shouldLogToFile = shouldWriteLogs(); if (shouldLogToFile) { try { - Files.createDirectories(usedLogsDir); + FileUtils.forceMkdir(usedLogsDir.toFile()); var file = usedLogsDir.resolve("xpipe.log"); var fos = new FileOutputStream(file.toFile(), true); var buf = new BufferedOutputStream(fos); 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 8150cc878..0a18c8d45 100644 --- a/app/src/main/java/io/xpipe/app/core/AppProperties.java +++ b/app/src/main/java/io/xpipe/app/core/AppProperties.java @@ -37,11 +37,13 @@ public class AppProperties { boolean useVirtualThreads; boolean debugThreads; Path dataDir; + Path defaultDataDir; boolean showcase; AppVersion canonicalVersion; boolean locatePtb; boolean locatorVersionCheck; boolean isTest; + boolean autoAcceptEula; public AppProperties() { var appDir = Path.of(System.getProperty("user.dir")).resolve("app"); @@ -86,6 +88,7 @@ public class AppProperties { debugThreads = Optional.ofNullable(System.getProperty("io.xpipe.app.debugThreads")) .map(Boolean::parseBoolean) .orElse(false); + defaultDataDir = Path.of(System.getProperty("user.home"), isStaging() ? ".xpipe-ptb" : ".xpipe"); dataDir = Optional.ofNullable(System.getProperty("io.xpipe.app.dataDir")) .map(s -> { var p = Path.of(s); @@ -94,7 +97,7 @@ public class AppProperties { } return p; }) - .orElse(Path.of(System.getProperty("user.home"), isStaging() ? ".xpipe-ptb" : ".xpipe")); + .orElse(defaultDataDir); showcase = Optional.ofNullable(System.getProperty("io.xpipe.app.showcase")) .map(Boolean::parseBoolean) .orElse(false); @@ -107,6 +110,9 @@ public class AppProperties { .map(s -> !Boolean.parseBoolean(s)) .orElse(true); isTest = isJUnitTest(); + autoAcceptEula = Optional.ofNullable(System.getProperty("io.xpipe.app.acceptEula")) + .map(Boolean::parseBoolean) + .orElse(false); } private static boolean isJUnitTest() { 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 017c4cf75..05cdb6cb8 100644 --- a/app/src/main/java/io/xpipe/app/core/AppTheme.java +++ b/app/src/main/java/io/xpipe/app/core/AppTheme.java @@ -43,6 +43,10 @@ public class AppTheme { public static void initThemeHandlers(Stage stage) { Runnable r = () -> { + stage.getScene() + .getRoot() + .pseudoClassStateChanged( + PseudoClass.getPseudoClass(OsType.getLocal().getId()), true); if (AppPrefs.get() == null) { var def = Theme.getDefaultLightTheme(); stage.getScene().getRoot().getStyleClass().add(def.getCssId()); @@ -109,6 +113,9 @@ public class AppTheme { } }); }); + } catch (UnsupportedOperationException ex) { + // The platform preferences are sometimes not initialized yet + ErrorEvent.fromThrowable(ex).expected().omit().handle(); } catch (Throwable t) { ErrorEvent.fromThrowable(t).omit().handle(); } @@ -132,6 +139,9 @@ public class AppTheme { } else { AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme()); } + } catch (UnsupportedOperationException ex) { + // The platform preferences are sometimes not initialized yet + ErrorEvent.fromThrowable(ex).expected().omit().handle(); } catch (Exception ex) { // The color scheme query can fail if the toolkit is not initialized properly AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme()); @@ -206,7 +216,6 @@ public class AppTheme { Application.setUserAgentStylesheet(Styles.toDataURI(builder.toString())); } - public List getAdditionalStylesheets() { return List.of(); } 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 5d1318b7a..7a641ccd7 100644 --- a/app/src/main/java/io/xpipe/app/core/AppTrayIcon.java +++ b/app/src/main/java/io/xpipe/app/core/AppTrayIcon.java @@ -90,7 +90,8 @@ public class AppTrayIcon { tray.add(this.trayIcon); fixBackground(); } catch (Exception e) { - ErrorEvent.fromThrowable("Unable to add TrayIcon", e).handle(); + // This can sometimes fail on Linux + ErrorEvent.fromThrowable("Unable to add TrayIcon", e).expected().handle(); } }); } diff --git a/app/src/main/java/io/xpipe/app/core/check/AppRosettaCheck.java b/app/src/main/java/io/xpipe/app/core/check/AppRosettaCheck.java new file mode 100644 index 000000000..f044ec126 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/core/check/AppRosettaCheck.java @@ -0,0 +1,30 @@ +package io.xpipe.app.core.check; + +import io.xpipe.app.core.AppProperties; +import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.util.LocalShell; +import io.xpipe.core.process.OsType; + +public class AppRosettaCheck { + + public static void check() throws Exception { + if (OsType.getLocal() != OsType.MACOS) { + return; + } + + if (!AppProperties.get().getArch().equals("x86_64")) { + return; + } + + var ret = LocalShell.getShell().command("sysctl -n sysctl.proc_translated").readStdoutIfPossible(); + if (ret.isEmpty()) { + return; + } + + if (ret.get().equals("1")) { + ErrorEvent.fromMessage("You are running the Intel version of XPipe on an Apple Silicon system." + + " There is a native build available that comes with much better performance." + + " Please install that one instead."); + } + } +} 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 ac5b58b3a..c3000bc37 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 @@ -6,6 +6,8 @@ import io.xpipe.core.process.OsType; import io.xpipe.core.process.ProcessControlProvider; import io.xpipe.core.process.ProcessOutputException; +import lombok.Value; + import java.util.Optional; public class AppShellCheck { @@ -17,15 +19,19 @@ public class AppShellCheck { .getEffectiveLocalDialect() .equals(ProcessControlProvider.get().getFallbackDialect()); if (err.isPresent() && canFallback) { - var msg = formatMessage(err.get()); + var msg = formatMessage(err.get().getMessage()); ErrorEvent.fromThrowable(new IllegalStateException(msg)).handle(); enableFallback(); err = selfTestErrorCheck(); } if (err.isPresent()) { - var msg = formatMessage(err.get()); - ErrorEvent.fromThrowable(new IllegalStateException(msg)).handle(); + var msg = formatMessage(err.get().getMessage()); + var event = ErrorEvent.fromThrowable(new IllegalStateException(msg)); + if (!err.get().isCanContinue()) { + event.term(); + } + event.handle(); } } @@ -71,17 +77,24 @@ public class AppShellCheck { LocalShell.init(); } - private static Optional selfTestErrorCheck() { + private static Optional selfTestErrorCheck() { try (var command = LocalShell.getShell().command("echo test").complex().start()) { var out = command.readStdoutOrThrow(); if (!out.equals("test")) { - return Optional.of("Expected \"test\", got \"" + out + "\""); + return Optional.of(new FailureResult("Expected \"test\", got \"" + out + "\"", true)); } } catch (ProcessOutputException ex) { - return Optional.of(ex.getOutput() != null ? ex.getOutput() : ex.toString()); + return Optional.of(new FailureResult(ex.getOutput() != null ? ex.getOutput() : ex.toString(), true)); } catch (Throwable t) { - return Optional.of(t.getMessage() != null ? t.getMessage() : t.toString()); + return Optional.of(new FailureResult(t.getMessage() != null ? t.getMessage() : t.toString(), false)); } return Optional.empty(); } + + @Value + private static class FailureResult { + + String message; + boolean canContinue; + } } diff --git a/app/src/main/java/io/xpipe/app/core/check/AppUserDirectoryCheck.java b/app/src/main/java/io/xpipe/app/core/check/AppUserDirectoryCheck.java index cf7684c91..2d974a3b6 100644 --- a/app/src/main/java/io/xpipe/app/core/check/AppUserDirectoryCheck.java +++ b/app/src/main/java/io/xpipe/app/core/check/AppUserDirectoryCheck.java @@ -2,6 +2,7 @@ package io.xpipe.app.core.check; import io.xpipe.app.core.AppProperties; import io.xpipe.app.issue.ErrorEvent; +import org.apache.commons.io.FileUtils; import java.io.IOException; import java.nio.file.Files; @@ -12,17 +13,18 @@ public class AppUserDirectoryCheck { var dataDirectory = AppProperties.get().getDataDir(); try { - Files.createDirectories(dataDirectory); + FileUtils.forceMkdir(dataDirectory.toFile()); var testDirectory = dataDirectory.resolve("permissions_check"); - Files.createDirectories(testDirectory); + FileUtils.forceMkdir(testDirectory.toFile()); + if (!Files.exists(testDirectory)) { + throw new IOException("Directory creation in user home directory failed silently"); + } Files.delete(testDirectory); // if (true) throw new IOException(); } catch (IOException e) { - ErrorEvent.fromThrowable( - new IOException( - "Unable to access directory " + dataDirectory - + ". Please make sure that you have the appropriate permissions and no Antivirus program is blocking the access. " - + "In case you use cloud storage, verify that your cloud storage is working and you are logged in.")) + ErrorEvent.fromThrowable("Unable to access directory " + dataDirectory + + ". Please make sure that you have the appropriate permissions and no Antivirus program is blocking the access. " + + "In case you use cloud storage, verify that your cloud storage is working and you are logged in.", e) .term() .expected() .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 ecc3e7d68..7e3d12647 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 @@ -7,6 +7,7 @@ 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.AppRosettaCheck; import io.xpipe.app.core.check.AppShellCheck; import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.ext.DataStoreProviders; @@ -51,6 +52,7 @@ public class BaseMode extends OperationMode { AppSid.init(); LocalShell.init(); AppShellCheck.check(); + AppRosettaCheck.check(); XPipeDistributionType.init(); AppPrefs.setLocalDefaultsIfNeeded(); // Initialize beacon server as we should be prepared for git askpass commands 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 10e6f9950..5d0e33fb4 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 @@ -86,6 +86,9 @@ public abstract class OperationMode { private static void setup(String[] args) { try { + // Register stage theming early to make it apply for any potential early popups + ModifiedStage.init(); + // Only for handling SIGTERM Runtime.getRuntime().addShutdownHook(new Thread(() -> { TrackEvent.info("Received SIGTERM externally"); @@ -96,7 +99,9 @@ public abstract class OperationMode { Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> { // It seems like a few exceptions are thrown in the quantum renderer // when in shutdown. We can ignore these - if (OperationMode.isInShutdown() && Platform.isFxApplicationThread() && ex instanceof NullPointerException) { + if (OperationMode.isInShutdown() + && Platform.isFxApplicationThread() + && ex instanceof NullPointerException) { return; } @@ -117,8 +122,6 @@ public abstract class OperationMode { AppExtensionManager.init(true); AppI18n.init(); AppPrefs.initLocal(); - // Register stage theming early to make it apply for any potential early popups - ModifiedStage.init(); AppBeaconServer.setupPort(); TrackEvent.info("Finished initial setup"); } catch (Throwable ex) { @@ -224,7 +227,7 @@ public abstract class OperationMode { CURRENT = null; r.run(); } catch (Throwable ex) { - ErrorEvent.fromThrowable(ex).build().handle(); + ErrorEvent.fromThrowable(ex).handle(); OperationMode.halt(1); } 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 0e77b66e1..b5cb69efe 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 @@ -8,6 +8,7 @@ import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.update.UpdateAvailableAlert; import io.xpipe.app.util.PlatformState; import io.xpipe.app.util.ThreadHelper; + import javafx.application.Application; public abstract class PlatformMode extends OperationMode { 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 83bed40c9..fb1b0f799 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 @@ -11,7 +11,7 @@ public class TrayMode extends PlatformMode { @Override public boolean isSupported() { - return !OsType.getLocal().equals(OsType.MACOS) + return OsType.getLocal().equals(OsType.WINDOWS) && super.isSupported() && Desktop.isDesktopSupported() && SystemTray.isSupported(); diff --git a/app/src/main/java/io/xpipe/app/core/window/AppMainWindow.java b/app/src/main/java/io/xpipe/app/core/window/AppMainWindow.java index e374c046e..0ed9470c5 100644 --- a/app/src/main/java/io/xpipe/app/core/window/AppMainWindow.java +++ b/app/src/main/java/io/xpipe/app/core/window/AppMainWindow.java @@ -16,6 +16,8 @@ import javafx.beans.property.SimpleBooleanProperty; import javafx.geometry.Rectangle2D; import javafx.scene.Scene; import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; import javafx.scene.layout.Region; import javafx.scene.paint.Color; @@ -167,8 +169,8 @@ public class AppMainWindow { e.consume(); }); - stage.addEventFilter(KeyEvent.KEY_PRESSED, event -> { - if (event.getCode().equals(KeyCode.Q) && event.isShortcutDown()) { + stage.addEventHandler(KeyEvent.KEY_PRESSED, event -> { + if (new KeyCodeCombination(KeyCode.Q, KeyCombination.SHORTCUT_DOWN).match(event)) { stage.close(); AppPrefs.get().closeBehaviour().getValue().run(); event.consume(); @@ -184,7 +186,7 @@ public class AppMainWindow { stage.setY(state.windowY); stage.setWidth(state.windowWidth); stage.setHeight(state.windowHeight); - // stage.setMaximized(state.maximized); + stage.setMaximized(state.maximized); TrackEvent.debug("Window loaded saved bounds"); } else if (!AppProperties.get().isShowcase()) { @@ -271,8 +273,8 @@ public class AppMainWindow { contentR.prefHeightProperty().bind(stage.getScene().heightProperty()); if (OsType.getLocal().equals(OsType.LINUX) || OsType.getLocal().equals(OsType.MACOS)) { - stage.getScene().addEventFilter(KeyEvent.KEY_PRESSED, event -> { - if (event.getCode().equals(KeyCode.W) && event.isShortcutDown()) { + stage.getScene().addEventHandler(KeyEvent.KEY_PRESSED, event -> { + if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(event)) { AppPrefs.get().closeBehaviour().getValue().run(); event.consume(); } diff --git a/app/src/main/java/io/xpipe/app/core/window/AppWindowHelper.java b/app/src/main/java/io/xpipe/app/core/window/AppWindowHelper.java index 795cd6075..320a05624 100644 --- a/app/src/main/java/io/xpipe/app/core/window/AppWindowHelper.java +++ b/app/src/main/java/io/xpipe/app/core/window/AppWindowHelper.java @@ -8,6 +8,7 @@ import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.InputHelper; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.process.OsType; + import javafx.application.Platform; import javafx.beans.value.ObservableValue; import javafx.css.PseudoClass; @@ -17,6 +18,8 @@ import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; import javafx.scene.layout.Pane; import javafx.scene.layout.Region; @@ -25,7 +28,6 @@ import javafx.scene.paint.Color; import javafx.scene.text.Text; import javafx.stage.Modality; import javafx.stage.Stage; -import javafx.stage.StageStyle; import java.util.Optional; import java.util.concurrent.CountDownLatch; @@ -142,8 +144,8 @@ public class AppWindowHelper { event.consume(); }); AppWindowBounds.fixInvalidStagePosition(s); - a.getDialogPane().getScene().addEventFilter(KeyEvent.KEY_PRESSED, event -> { - if (event.getCode().equals(KeyCode.W) && event.isShortcutDown()) { + a.getDialogPane().getScene().addEventHandler(KeyEvent.KEY_PRESSED, event -> { + if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(event)) { s.close(); event.consume(); return; @@ -260,8 +262,8 @@ public class AppWindowHelper { } }); - scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> { - if (event.getCode().equals(KeyCode.W) && event.isShortcutDown()) { + scene.addEventHandler(KeyEvent.KEY_PRESSED, event -> { + if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(event)) { stage.close(); event.consume(); } diff --git a/app/src/main/java/io/xpipe/app/core/window/ModifiedStage.java b/app/src/main/java/io/xpipe/app/core/window/ModifiedStage.java index 960b678a1..171af3d0d 100644 --- a/app/src/main/java/io/xpipe/app/core/window/ModifiedStage.java +++ b/app/src/main/java/io/xpipe/app/core/window/ModifiedStage.java @@ -14,21 +14,16 @@ import javafx.stage.StageStyle; import javafx.stage.Window; import javafx.util.Duration; -import lombok.SneakyThrows; import org.apache.commons.lang3.SystemUtils; public class ModifiedStage extends Stage { public static boolean mergeFrame() { - return SystemUtils.IS_OS_WINDOWS_11; + return SystemUtils.IS_OS_WINDOWS_11 || SystemUtils.IS_OS_MAC; } - @SneakyThrows - @SuppressWarnings("unchecked") public static void init() { - var windowsField = Window.class.getDeclaredField("windows"); - windowsField.setAccessible(true); - ObservableList list = (ObservableList) windowsField.get(null); + ObservableList list = Window.getWindows(); list.addListener((ListChangeListener) c -> { if (c.next() && c.wasAdded()) { var added = c.getAddedSubList().getFirst(); @@ -62,24 +57,50 @@ public class ModifiedStage extends Stage { return; } - if (OsType.getLocal() != OsType.WINDOWS || AppPrefs.get() == null || AppPrefs.get().theme.getValue() == null) { + var applyToStage = (OsType.getLocal() == OsType.WINDOWS) + || (OsType.getLocal() == OsType.MACOS + && AppMainWindow.getInstance() != null + && AppMainWindow.getInstance().getStage() == stage); + if (!applyToStage || AppPrefs.get() == null || AppPrefs.get().theme.getValue() == null) { stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), false); stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), true); return; } - var ctrl = new NativeWinWindowControl(stage); - ctrl.setWindowAttribute( - NativeWinWindowControl.DmwaWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE.get(), - AppPrefs.get().theme.getValue().isDark()); - boolean seamlessFrame; - if (AppPrefs.get().performanceMode().get() || !mergeFrame()) { - seamlessFrame = false; - } else { - seamlessFrame = ctrl.setWindowBackdrop(NativeWinWindowControl.DwmSystemBackDropType.MICA_ALT); + switch (OsType.getLocal()) { + case OsType.Linux linux -> {} + case OsType.MacOs macOs -> { + var ctrl = new NativeMacOsWindowControl(stage); + var seamlessFrame = !AppPrefs.get().performanceMode().get() && mergeFrame(); + var seamlessFrameApplied = ctrl.setAppearance( + seamlessFrame, AppPrefs.get().theme.getValue().isDark()) + && seamlessFrame; + stage.getScene() + .getRoot() + .pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrameApplied); + stage.getScene() + .getRoot() + .pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrameApplied); + } + case OsType.Windows windows -> { + var ctrl = new NativeWinWindowControl(stage); + ctrl.setWindowAttribute( + NativeWinWindowControl.DmwaWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE.get(), + AppPrefs.get().theme.getValue().isDark()); + boolean seamlessFrame; + if (AppPrefs.get().performanceMode().get() || !mergeFrame()) { + seamlessFrame = false; + } else { + seamlessFrame = ctrl.setWindowBackdrop(NativeWinWindowControl.DwmSystemBackDropType.MICA_ALT); + } + stage.getScene() + .getRoot() + .pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrame); + stage.getScene() + .getRoot() + .pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrame); + } } - stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrame); - stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrame); } private static void updateStage(Stage stage) { diff --git a/app/src/main/java/io/xpipe/app/core/window/NativeMacOsWindowControl.java b/app/src/main/java/io/xpipe/app/core/window/NativeMacOsWindowControl.java new file mode 100644 index 000000000..30dbc0c27 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/core/window/NativeMacOsWindowControl.java @@ -0,0 +1,56 @@ +package io.xpipe.app.core.window; + +import io.xpipe.app.core.AppProperties; +import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.util.NativeBridge; +import io.xpipe.app.util.ThreadHelper; +import io.xpipe.core.util.ModuleHelper; + +import javafx.stage.Window; + +import com.sun.jna.NativeLong; +import lombok.Getter; +import lombok.SneakyThrows; + +import java.lang.reflect.Method; + +@Getter +public class NativeMacOsWindowControl { + + private final long nsWindow; + + @SneakyThrows + public NativeMacOsWindowControl(Window stage) { + Method tkStageGetter = Window.class.getDeclaredMethod("getPeer"); + tkStageGetter.setAccessible(true); + Object tkStage = tkStageGetter.invoke(stage); + Method getPlatformWindow = tkStage.getClass().getDeclaredMethod("getPlatformWindow"); + getPlatformWindow.setAccessible(true); + Object platformWindow = getPlatformWindow.invoke(tkStage); + Method getNativeHandle = platformWindow.getClass().getMethod("getNativeHandle"); + getNativeHandle.setAccessible(true); + Object nativeHandle = getNativeHandle.invoke(platformWindow); + this.nsWindow = (long) nativeHandle; + } + + public boolean setAppearance(boolean seamlessFrame, boolean darkMode) { + if (!ModuleHelper.isImage() || !AppProperties.get().isFullVersion()) { + return false; + } + + var lib = NativeBridge.getMacOsLibrary(); + if (lib.isEmpty()) { + return false; + } + + try { + lib.get().setAppearance(new NativeLong(nsWindow), seamlessFrame, darkMode); + if (seamlessFrame) { + ThreadHelper.sleep(100); + } + } catch (Throwable e) { + ErrorEvent.fromThrowable(e).handle(); + } + return true; + } +} 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 4e8291b68..117a6db55 100644 --- a/app/src/main/java/io/xpipe/app/ext/ActionProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/ActionProvider.java @@ -16,6 +16,7 @@ import java.util.ServiceLoader; public interface ActionProvider { List ALL = new ArrayList<>(); + List ALL_STANDALONE = new ArrayList<>(); static void initProviders() { for (ActionProvider actionProvider : ALL) { @@ -111,7 +112,7 @@ public interface ActionProvider { String getIcon(DataStoreEntryRef store); - Class getApplicableClass(); + Class getApplicableClass(); default boolean showBusy() { return true; @@ -120,9 +121,11 @@ public interface ActionProvider { interface BranchDataStoreCallSite extends DataStoreCallSite { - default List getChildren() { - return List.of(); + default boolean isDynamicallyGenerated(){ + return false; } + + List getChildren(DataStoreEntryRef store); } interface LeafDataStoreCallSite extends DataStoreCallSite { @@ -145,6 +148,18 @@ public interface ActionProvider { ALL.addAll(ServiceLoader.load(layer, ActionProvider.class).stream() .map(actionProviderProvider -> actionProviderProvider.get()) .toList()); + + var menuProviders = ALL.stream() + .map(actionProvider -> actionProvider.getBranchDataStoreCallSite() != null && + !actionProvider.getBranchDataStoreCallSite().isDynamicallyGenerated() + ? actionProvider.getBranchDataStoreCallSite().getChildren(null) + : List.of()) + .flatMap(List::stream) + .toList(); + ALL_STANDALONE.addAll(ALL.stream() + .filter(actionProvider -> menuProviders.stream() + .noneMatch(menuItem -> menuItem.getClass().equals(actionProvider.getClass()))) + .toList()); } } } diff --git a/app/src/main/java/io/xpipe/app/ext/DataStoreCreationCategory.java b/app/src/main/java/io/xpipe/app/ext/DataStoreCreationCategory.java index 46c304c11..17c863dd9 100644 --- a/app/src/main/java/io/xpipe/app/ext/DataStoreCreationCategory.java +++ b/app/src/main/java/io/xpipe/app/ext/DataStoreCreationCategory.java @@ -9,5 +9,6 @@ public enum DataStoreCreationCategory { TUNNEL, SCRIPT, CLUSTER, - DESKTOP + DESKTOP, + SERIAL } 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 9f27e842b..393e9fbbb 100644 --- a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java @@ -10,7 +10,6 @@ import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppImages; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.issue.ErrorEvent; -import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.core.store.DataStore; import io.xpipe.core.util.JacksonizedValue; @@ -27,10 +26,16 @@ import java.util.List; public interface DataStoreProvider { + default boolean showProviderChoice() { + return true; + } + default boolean shouldShow(StoreEntryWrapper w) { return true; } + default void onParentRefresh(DataStoreEntry entry) {} + default void onChildrenRefresh(DataStoreEntry entry) {} default ObservableBooleanValue busy(StoreEntryWrapper wrapper) { @@ -71,21 +76,16 @@ public interface DataStoreProvider { return null; } - default String browserDisplayName(DataStore store) { - var e = DataStorage.get().getStoreDisplayName(store); - return e.orElse("?"); + default String displayName(DataStoreEntry entry) { + return entry.getName(); } default List getSearchableTerms(DataStore store) { return List.of(); } - default boolean shouldEdit() { - return false; - } - default StoreEntryComp customEntryComp(StoreSection s, boolean preferLarge) { - return StoreEntryComp.create(s.getWrapper(), null, preferLarge); + return StoreEntryComp.create(s, null, preferLarge); } default StoreSectionComp customSectionComp(StoreSection section, boolean topLevel) { @@ -104,6 +104,10 @@ public interface DataStoreProvider { return Comp.empty(); } + default boolean canConnectDuringCreation() { + return false; + } + default Comp createInsightsComp(ObservableValue store) { var content = Bindings.createStringBinding( () -> { @@ -152,6 +156,10 @@ public interface DataStoreProvider { return DataStoreUsageCategory.DATABASE; } + if (cc == DataStoreCreationCategory.SERIAL) { + return DataStoreUsageCategory.SERIAL; + } + return null; } @@ -191,7 +199,7 @@ public interface DataStoreProvider { return null; } - default ObservableValue informationString(StoreEntryWrapper wrapper) { + default ObservableValue informationString(StoreSection section) { return new SimpleStringProperty(null); } 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 85827c093..287253f87 100644 --- a/app/src/main/java/io/xpipe/app/ext/DataStoreProviders.java +++ b/app/src/main/java/io/xpipe/app/ext/DataStoreProviders.java @@ -68,7 +68,10 @@ public class DataStoreProviders { throw new IllegalStateException("Not initialized"); } - return (T) ALL.stream().filter(d -> d.getStoreClasses().contains(store.getClass())).findAny().orElseThrow(() -> new IllegalArgumentException("Unknown store class")); + return (T) ALL.stream() + .filter(d -> d.getStoreClasses().contains(store.getClass())) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("Unknown store class")); } public static List getAll() { diff --git a/app/src/main/java/io/xpipe/app/ext/DataStoreUsageCategory.java b/app/src/main/java/io/xpipe/app/ext/DataStoreUsageCategory.java index d88f724df..7257840bf 100644 --- a/app/src/main/java/io/xpipe/app/ext/DataStoreUsageCategory.java +++ b/app/src/main/java/io/xpipe/app/ext/DataStoreUsageCategory.java @@ -16,5 +16,7 @@ public enum DataStoreUsageCategory { @JsonProperty("desktop") DESKTOP, @JsonProperty("group") - GROUP; + GROUP, + @JsonProperty("serial") + SERIAL; } diff --git a/app/src/main/java/io/xpipe/app/ext/EnabledParentStoreProvider.java b/app/src/main/java/io/xpipe/app/ext/EnabledParentStoreProvider.java index 22e2c1a4d..d1f1e59e8 100644 --- a/app/src/main/java/io/xpipe/app/ext/EnabledParentStoreProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/EnabledParentStoreProvider.java @@ -14,8 +14,8 @@ public interface EnabledParentStoreProvider extends DataStoreProvider { @Override default StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) { - if (sec.getWrapper().getValidity().getValue() != DataStoreEntry.Validity.COMPLETE) { - return StoreEntryComp.create(sec.getWrapper(), null, preferLarge); + if (sec.getWrapper().getValidity().getValue() == DataStoreEntry.Validity.LOAD_FAILED) { + return StoreEntryComp.create(sec, null, preferLarge); } var enabled = StoreToggleComp.>enableToggle( @@ -35,6 +35,6 @@ public interface EnabledParentStoreProvider extends DataStoreProvider { })); } - return StoreEntryComp.create(sec.getWrapper(), enabled, preferLarge); + return StoreEntryComp.create(sec, enabled, preferLarge); } } diff --git a/app/src/main/java/io/xpipe/app/ext/EnabledStoreProvider.java b/app/src/main/java/io/xpipe/app/ext/EnabledStoreProvider.java index cf236e9ad..2a9a4f899 100644 --- a/app/src/main/java/io/xpipe/app/ext/EnabledStoreProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/EnabledStoreProvider.java @@ -11,8 +11,8 @@ public interface EnabledStoreProvider extends DataStoreProvider { @Override default StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) { - if (sec.getWrapper().getValidity().getValue() != DataStoreEntry.Validity.COMPLETE) { - return StoreEntryComp.create(sec.getWrapper(), null, preferLarge); + if (sec.getWrapper().getValidity().getValue() == DataStoreEntry.Validity.LOAD_FAILED) { + return StoreEntryComp.create(sec, null, preferLarge); } var enabled = StoreToggleComp.>enableToggle( @@ -20,6 +20,6 @@ public interface EnabledStoreProvider extends DataStoreProvider { var state = s.getState().toBuilder().enabled(aBoolean).build(); s.setState(state); }); - return StoreEntryComp.create(sec.getWrapper(), enabled, preferLarge); + return StoreEntryComp.create(sec, enabled, preferLarge); } } 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 3ed8260f4..965f1147e 100644 --- a/app/src/main/java/io/xpipe/app/ext/ScanProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/ScanProvider.java @@ -2,10 +2,8 @@ package io.xpipe.app.ext; import io.xpipe.app.storage.DataStoreEntry; 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; @@ -22,10 +20,6 @@ public abstract class ScanProvider { 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/SingletonSessionStoreProvider.java b/app/src/main/java/io/xpipe/app/ext/SingletonSessionStoreProvider.java index 0dc04c93d..c856c0121 100644 --- a/app/src/main/java/io/xpipe/app/ext/SingletonSessionStoreProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/SingletonSessionStoreProvider.java @@ -30,7 +30,7 @@ public interface SingletonSessionStoreProvider extends DataStoreProvider { @Override default StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) { var t = createToggleComp(sec); - return StoreEntryComp.create(sec.getWrapper(), t, preferLarge); + return StoreEntryComp.create(sec, t, preferLarge); } default StoreToggleComp createToggleComp(StoreSection sec) { diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/AnchorComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/AnchorComp.java new file mode 100644 index 000000000..393d30935 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/AnchorComp.java @@ -0,0 +1,28 @@ +package io.xpipe.app.fxcomps.impl; + +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.CompStructure; +import io.xpipe.app.fxcomps.SimpleCompStructure; + +import javafx.scene.layout.AnchorPane; + +import java.util.List; + +public class AnchorComp extends Comp> { + + private final List> comps; + + public AnchorComp(List> comps) { + this.comps = List.copyOf(comps); + } + + @Override + public CompStructure createBase() { + var pane = new AnchorPane(); + for (var c : comps) { + pane.getChildren().add(c.createRegion()); + } + pane.setPickOnBounds(false); + return new SimpleCompStructure<>(pane); + } +} 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 77d47c38e..50a5b0fdd 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 @@ -1,5 +1,6 @@ package io.xpipe.app.fxcomps.impl; +import atlantafx.base.theme.Styles; import io.xpipe.app.browser.session.BrowserChooserComp; import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.core.AppI18n; @@ -15,39 +16,29 @@ import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileSystemStore; - import javafx.application.Platform; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ObservableValue; import javafx.scene.control.Alert; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; - -import atlantafx.base.theme.Styles; import org.kordamp.ikonli.javafx.FontIcon; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; -import java.util.List; +import java.util.ArrayList; public class ContextualFileReferenceChoiceComp extends Comp> { private final Property> fileSystem; private final Property filePath; + private final boolean allowSync; public ContextualFileReferenceChoiceComp( - ObservableValue> fileSystem, Property filePath) { - this.fileSystem = new SimpleObjectProperty<>(); - fileSystem.subscribe(val -> { - this.fileSystem.setValue(val); - }); - this.filePath = filePath; - } - - public ContextualFileReferenceChoiceComp( - Property> fileSystem, Property filePath) { + Property> fileSystem, Property filePath, boolean allowSync + ) { + this.allowSync = allowSync; this.fileSystem = new SimpleObjectProperty<>(); fileSystem.subscribe(val -> { this.fileSystem.setValue(val); @@ -79,7 +70,7 @@ public class ContextualFileReferenceChoiceComp extends Comp> }, false); }) - .styleClass(Styles.CENTER_PILL) + .styleClass(allowSync ? Styles.CENTER_PILL : Styles.RIGHT_PILL) .grow(false, true); var gitShareButton = new ButtonComp(null, new FontIcon("mdi2g-git"), () -> { @@ -126,7 +117,13 @@ public class ContextualFileReferenceChoiceComp extends Comp> gitShareButton.tooltipKey("gitShareFileTooltip"); gitShareButton.styleClass(Styles.RIGHT_PILL).grow(false, true); - var layout = new HorizontalComp(List.of(fileNameComp, fileBrowseButton, gitShareButton)) + var nodes = new ArrayList>(); + nodes.add(fileNameComp); + nodes.add(fileBrowseButton); + if (allowSync) { + nodes.add(gitShareButton); + } + var layout = new HorizontalComp(nodes) .apply(struc -> struc.get().setFillHeight(true)); layout.apply(struc -> { 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 54560b961..929d87835 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 @@ -1,7 +1,5 @@ package io.xpipe.app.fxcomps.impl; -import atlantafx.base.controls.Popover; -import atlantafx.base.theme.Styles; import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.comp.store.*; import io.xpipe.app.core.AppFont; @@ -15,6 +13,7 @@ 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; import javafx.beans.property.SimpleBooleanProperty; @@ -26,6 +25,9 @@ import javafx.scene.control.MenuButton; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; + +import atlantafx.base.controls.Popover; +import atlantafx.base.theme.Styles; import lombok.RequiredArgsConstructor; import org.kordamp.ikonli.javafx.FontIcon; @@ -101,9 +103,9 @@ public class DataStoreChoiceComp extends SimpleComp { comp.disable(new SimpleBooleanProperty(true)); } }, - storeEntryWrapper -> { - if (applicable.test(storeEntryWrapper)) { - selected.setValue(storeEntryWrapper.getEntry().ref()); + sec -> { + if (applicable.test(sec.getWrapper())) { + selected.setValue(sec.getWrapper().getEntry().ref()); popover.hide(); } }); @@ -112,22 +114,31 @@ public class DataStoreChoiceComp extends SimpleComp { StoreViewState.get().getActiveCategory(), selectedCategory) .styleClass(Styles.LEFT_PILL); - var filter = - new FilterComp(filterText).styleClass(Styles.CENTER_PILL).hgrow(); + 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")); + m.setMaxHeight(100); + m.setMinHeight(0); StoreCreationMenu.addButtons(m); return m; }) .accessibleTextKey("addConnection") - .padding(new Insets(-2)) - .styleClass(Styles.RIGHT_PILL) - .grow(false, true); + .padding(new Insets(-5)) + .styleClass(Styles.RIGHT_PILL); - var top = new HorizontalComp(List.of(category, filter.hgrow(), addButton)) + var top = new HorizontalComp(List.of(category, filter, addButton)) .styleClass("top") .apply(struc -> struc.get().setFillHeight(true)) + .apply(struc -> { + var first = ((Region) struc.get().getChildren().get(0)); + var second = ((Region) struc.get().getChildren().get(1)); + var third = ((Region) struc.get().getChildren().get(1)); + second.prefHeightProperty().bind(first.heightProperty()); + second.minHeightProperty().bind(first.heightProperty()); + second.maxHeightProperty().bind(first.heightProperty()); + third.prefHeightProperty().bind(first.heightProperty()); + }) .apply(struc -> { // Ugly solution to focus the text field // Somehow this does not work through the normal on shown listeners 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 552ac4ba7..2e901e5e8 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 @@ -53,7 +53,7 @@ public class DataStoreListChoiceComp extends SimpleComp { }); return new HorizontalComp(List.of(label, Comp.hspacer(), delete)).styleClass("entry"); }, - true) + false) .padding(new Insets(0)) .apply(struc -> struc.get().setMinHeight(0)) .apply(struc -> ((VBox) struc.get().getContent()).setSpacing(5)); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/FilterComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/FilterComp.java index 4aa431338..8a131e0f2 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/FilterComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/FilterComp.java @@ -1,17 +1,21 @@ package io.xpipe.app.fxcomps.impl; -import atlantafx.base.controls.CustomTextField; import io.xpipe.app.core.AppActionLinkDetector; import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; + import javafx.beans.binding.Bindings; import javafx.beans.property.Property; -import javafx.geometry.Pos; import javafx.scene.Cursor; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseButton; + +import atlantafx.base.controls.CustomTextField; import org.kordamp.ikonli.javafx.FontIcon; import java.util.Objects; @@ -36,16 +40,29 @@ public class FilterComp extends Comp> { } }); var filter = new CustomTextField(); - filter.alignmentProperty().bind(Bindings.createObjectBinding(() -> { - return filter.isFocused() || (filter.getText() != null && !filter.getText().isEmpty()) ? Pos.CENTER_LEFT : Pos.CENTER; - }, filter.textProperty(), filter.focusedProperty())); + filter.setMinHeight(0); filter.setMaxHeight(2000); filter.getStyleClass().add("filter-comp"); filter.promptTextProperty().bind(AppI18n.observable("searchFilter")); - filter.setLeft(fi); - filter.setRight(clear); + filter.rightProperty() + .bind(Bindings.createObjectBinding( + () -> { + return filter.isFocused() + || (filter.getText() != null + && !filter.getText().isEmpty()) + ? clear + : fi; + }, + filter.focusedProperty())); filter.setAccessibleText("Filter"); + filter.addEventFilter(KeyEvent.KEY_PRESSED,event -> { + if (new KeyCodeCombination(KeyCode.ESCAPE).match(event)) { + filter.getScene().getRoot().requestFocus(); + event.consume(); + } + }); + filterText.subscribe(val -> { PlatformThread.runLaterIfNeeded(() -> { clear.setVisible(val != null); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/HorizontalComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/HorizontalComp.java index 1ecb7883f..2ab9f562a 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/HorizontalComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/HorizontalComp.java @@ -4,17 +4,25 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; +import io.xpipe.app.fxcomps.util.DerivedObservableList; +import io.xpipe.app.fxcomps.util.PlatformThread; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; import javafx.geometry.Pos; import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import lombok.AllArgsConstructor; import java.util.List; +@AllArgsConstructor public class HorizontalComp extends Comp> { - private final List> entries; + private final ObservableList> entries; public HorizontalComp(List> comps) { - entries = List.copyOf(comps); + entries = FXCollections.observableList(List.copyOf(comps)); } public Comp> spacing(double spacing) { @@ -25,9 +33,13 @@ public class HorizontalComp extends Comp> { public CompStructure createBase() { HBox b = new HBox(); b.getStyleClass().add("horizontal-comp"); - for (var entry : entries) { - b.getChildren().add(entry.createRegion()); - } + var map = new DerivedObservableList<>(entries, false).mapped(comp -> comp.createRegion()).getList(); + b.getChildren().setAll(map); + map.addListener((ListChangeListener) c -> { + PlatformThread.runLaterIfNeeded(() -> { + b.getChildren().setAll(c.getList()); + }); + }); b.setAlignment(Pos.CENTER); return new SimpleCompStructure<>(b); } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/IconButtonComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/IconButtonComp.java index 438247b0e..2bc0961b0 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/IconButtonComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/IconButtonComp.java @@ -1,38 +1,42 @@ package io.xpipe.app.fxcomps.impl; +import atlantafx.base.theme.Styles; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; +import io.xpipe.app.fxcomps.util.LabelGraphic; import io.xpipe.app.fxcomps.util.PlatformThread; - import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; -import javafx.css.Size; -import javafx.css.SizeUnits; import javafx.scene.control.Button; -import atlantafx.base.theme.Styles; -import org.kordamp.ikonli.javafx.FontIcon; - public class IconButtonComp extends Comp> { - private final ObservableValue icon; + private final ObservableValue icon; private final Runnable listener; public IconButtonComp(String defaultVal) { + this(new SimpleObjectProperty<>(new LabelGraphic.IconGraphic(defaultVal)), null); + } + + public IconButtonComp(String defaultVal, Runnable listener) { + this(new SimpleObjectProperty<>(new LabelGraphic.IconGraphic(defaultVal)), listener); + } + + public IconButtonComp(LabelGraphic defaultVal) { this(new SimpleObjectProperty<>(defaultVal), null); } - public IconButtonComp(ObservableValue icon) { + public IconButtonComp(ObservableValue icon) { this.icon = icon; this.listener = null; } - public IconButtonComp(String defaultVal, Runnable listener) { + public IconButtonComp(LabelGraphic defaultVal, Runnable listener) { this(new SimpleObjectProperty<>(defaultVal), listener); } - public IconButtonComp(ObservableValue icon, Runnable listener) { + public IconButtonComp(ObservableValue icon, Runnable listener) { this.icon = PlatformThread.sync(icon); this.listener = listener; } @@ -42,17 +46,20 @@ public class IconButtonComp extends Comp> { var button = new Button(); button.getStyleClass().add(Styles.FLAT); - var fi = new FontIcon(icon.getValue()); - fi.setFocusTraversable(false); - icon.addListener((c, o, n) -> { - fi.setIconLiteral(n); +// var fi = new FontIcon(icon.getValue()); +// fi.setFocusTraversable(false); +// icon.addListener((c, o, n) -> { +// fi.setIconLiteral(n); +// }); +// fi.setIconSize((int) new Size(fi.getFont().getSize(), SizeUnits.PT).pixels()); +// button.fontProperty().addListener((c, o, n) -> { +// fi.setIconSize((int) new Size(n.getSize(), SizeUnits.PT).pixels()); +// }); +// // fi.iconColorProperty().bind(button.textFillProperty()); +// button.setGraphic(fi); + icon.subscribe(labelGraphic -> { + button.setGraphic(labelGraphic.createGraphicNode()); }); - fi.setIconSize((int) new Size(fi.getFont().getSize(), SizeUnits.PT).pixels()); - button.fontProperty().addListener((c, o, n) -> { - fi.setIconSize((int) new Size(n.getSize(), SizeUnits.PT).pixels()); - }); - // fi.iconColorProperty().bind(button.textFillProperty()); - button.setGraphic(fi); if (listener != null) { button.setOnAction(e -> { listener.run(); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/IntComboFieldComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/IntComboFieldComp.java new file mode 100644 index 000000000..318888a82 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/IntComboFieldComp.java @@ -0,0 +1,79 @@ +package io.xpipe.app.fxcomps.impl; + +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.CompStructure; +import io.xpipe.app.fxcomps.SimpleCompStructure; +import io.xpipe.app.fxcomps.util.PlatformThread; +import javafx.beans.property.Property; +import javafx.beans.value.ChangeListener; +import javafx.collections.FXCollections; +import javafx.scene.control.ComboBox; +import javafx.scene.control.skin.ComboBoxListViewSkin; +import javafx.scene.input.KeyEvent; +import lombok.AccessLevel; +import lombok.experimental.FieldDefaults; + +import java.util.List; + +@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) +public class IntComboFieldComp extends Comp>> { + + Property value; + List predefined; + boolean allowNegative; + + public IntComboFieldComp(Property value, List predefined, boolean allowNegative) { + this.value = value; + this.predefined = predefined; + this.allowNegative = allowNegative; + } + + @Override + public CompStructure> createBase() { + var text = new ComboBox(); + text.setEditable(true); + text.setValue(value.getValue() != null ? value.getValue().toString() : null); + text.setItems(FXCollections.observableList(predefined.stream().map(integer -> "" + integer).toList())); + text.setMaxWidth(2000); + text.getStyleClass().add("int-combo-field-comp"); + text.setSkin(new ComboBoxListViewSkin<>(text)); + text.setVisibleRowCount(Math.min(10, predefined.size())); + + value.addListener((ChangeListener) (observableValue, oldValue, newValue) -> { + PlatformThread.runLaterIfNeeded(() -> { + if (newValue == null) { + text.setValue(""); + } else { + text.setValue(newValue.toString()); + } + }); + }); + + text.addEventFilter(KeyEvent.KEY_TYPED, keyEvent -> { + if (allowNegative) { + if (!"-0123456789".contains(keyEvent.getCharacter())) { + keyEvent.consume(); + } + } else { + if (!"0123456789".contains(keyEvent.getCharacter())) { + keyEvent.consume(); + } + } + }); + + text.valueProperty().addListener((observableValue, oldValue, newValue) -> { + if (newValue == null + || newValue.isEmpty() + || (allowNegative && "-".equals(newValue)) + || !newValue.matches("-?\\d+")) { + value.setValue(null); + return; + } + + int intValue = Integer.parseInt(newValue); + value.setValue(intValue); + }); + + return new SimpleCompStructure<>(text); + } +} 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 6c6670015..0beffef90 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 @@ -4,12 +4,10 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; - import javafx.beans.property.Property; import javafx.beans.value.ChangeListener; import javafx.scene.control.TextField; import javafx.scene.input.KeyEvent; - import lombok.AccessLevel; import lombok.experimental.FieldDefaults; 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 223d0db1f..573da420c 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 @@ -33,13 +33,6 @@ public class OptionsComp extends Comp> { this.entries = entries; } - public OptionsComp.Entry queryEntry(String key) { - return entries.stream() - .filter(entry -> entry.key != null && entry.key.equals(key)) - .findAny() - .orElseThrow(); - } - @Override public CompStructure createBase() { Pane pane; @@ -70,6 +63,7 @@ public class OptionsComp extends Comp> { name.getStyleClass().add("name"); name.textProperty().bind(entry.name()); name.setMinWidth(Region.USE_PREF_SIZE); + name.setMinHeight(Region.USE_PREF_SIZE); name.setAlignment(Pos.CENTER_LEFT); if (compRegion != null) { name.visibleProperty().bind(PlatformThread.sync(compRegion.visibleProperty())); @@ -82,6 +76,7 @@ public class OptionsComp extends Comp> { description.getStyleClass().add("description"); description.textProperty().bind(entry.description()); description.setAlignment(Pos.CENTER_LEFT); + description.setMinHeight(Region.USE_PREF_SIZE); if (compRegion != null) { description.visibleProperty().bind(PlatformThread.sync(compRegion.visibleProperty())); description.managedProperty().bind(PlatformThread.sync(compRegion.managedProperty())); 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 c5919a83d..5312414c6 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 @@ -12,6 +12,7 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.util.DerivedObservableList; +import io.xpipe.app.fxcomps.util.LabelGraphic; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreCategory; import io.xpipe.app.util.ContextMenuHelper; @@ -45,14 +46,14 @@ public class StoreCategoryComp extends SimpleComp { @Override protected Region createSimple() { - var i = Bindings.createStringBinding( + var i = Bindings.createObjectBinding( () -> { if (!DataStorage.get().supportsSharing() || !category.getCategory().canShare()) { - return "mdal-keyboard_arrow_right"; + return new LabelGraphic.IconGraphic("mdal-keyboard_arrow_right"); } - return category.getShare().getValue() ? "mdi2a-account-convert" : "mdi2a-account-cancel"; + return new LabelGraphic.IconGraphic(category.getShare().getValue() ? "mdi2a-account-convert" : "mdi2a-account-cancel"); }, category.getShare()); var icon = new IconButtonComp(i) diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/StringSourceComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/StringSourceComp.java deleted file mode 100644 index 08252bb2c..000000000 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/StringSourceComp.java +++ /dev/null @@ -1,63 +0,0 @@ -package io.xpipe.app.fxcomps.impl; - -import io.xpipe.app.fxcomps.SimpleComp; -import io.xpipe.app.storage.DataStoreEntryRef; -import io.xpipe.app.util.StringSource; -import io.xpipe.core.store.ShellStore; - -import javafx.beans.property.Property; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ObservableValue; -import javafx.scene.layout.AnchorPane; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; - -public class StringSourceComp extends SimpleComp { - - private final Property> fileSystem; - private final Property stringSource; - - public StringSourceComp( - ObservableValue> fileSystem, Property stringSource) { - this.stringSource = stringSource; - this.fileSystem = new SimpleObjectProperty<>(); - fileSystem.subscribe(val -> { - this.fileSystem.setValue(val.get().ref()); - }); - } - - @Override - protected Region createSimple() { - var inPlace = - new SimpleObjectProperty<>(stringSource.getValue() instanceof StringSource.InPlace i ? i.get() : null); - var fs = stringSource.getValue() instanceof StringSource.File f ? f.getFile() : null; - var file = new SimpleObjectProperty<>( - stringSource.getValue() instanceof StringSource.File f - ? f.getFile().serialize() - : null); - var showText = new SimpleBooleanProperty(inPlace.get() != null); - - var stringField = new TextAreaComp(inPlace); - stringField.hide(showText.not()); - var fileComp = new ContextualFileReferenceChoiceComp(fileSystem, file); - fileComp.hide(showText); - - var tr = stringField.createRegion(); - var button = new IconButtonComp("mdi2c-checkbox-marked-outline", () -> { - showText.set(!showText.getValue()); - }) - .createRegion(); - AnchorPane.setBottomAnchor(button, 10.0); - AnchorPane.setRightAnchor(button, 10.0); - var anchorPane = new AnchorPane(tr, button); - AnchorPane.setBottomAnchor(tr, 0.0); - AnchorPane.setTopAnchor(tr, 0.0); - AnchorPane.setLeftAnchor(tr, 0.0); - AnchorPane.setRightAnchor(tr, 0.0); - - var fr = fileComp.createRegion(); - - return new StackPane(tr, fr); - } -} diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/TooltipAugment.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/TooltipAugment.java index e7c7aeabf..67d820806 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/TooltipAugment.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/TooltipAugment.java @@ -10,6 +10,7 @@ import javafx.beans.value.ObservableValue; import javafx.scene.control.Tooltip; import javafx.scene.input.KeyCombination; import javafx.stage.Window; +import javafx.util.Duration; public class TooltipAugment> implements Augment { @@ -45,6 +46,7 @@ public class TooltipAugment> implements Augment { tt.setWrapText(true); tt.setMaxWidth(400); tt.getStyleClass().add("fancy-tooltip"); + tt.setHideDelay(Duration.INDEFINITE); Tooltip.install(struc.get(), tt); } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/util/LabelGraphic.java b/app/src/main/java/io/xpipe/app/fxcomps/util/LabelGraphic.java index c16e46484..ab04874b9 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/util/LabelGraphic.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/util/LabelGraphic.java @@ -1,9 +1,8 @@ package io.xpipe.app.fxcomps.util; import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.impl.PrettyImageHelper; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ObservableValue; import javafx.scene.Node; import lombok.EqualsAndHashCode; @@ -12,8 +11,14 @@ import org.kordamp.ikonli.javafx.FontIcon; public abstract class LabelGraphic { - public static ObservableValue fixedIcon(String icon) { - return new SimpleObjectProperty<>(new IconGraphic(icon)); + public static LabelGraphic none() { + return new LabelGraphic() { + + @Override + public Node createGraphicNode() { + return null; + } + }; } public abstract Node createGraphicNode(); @@ -30,6 +35,19 @@ public abstract class LabelGraphic { } } + @Value + @EqualsAndHashCode(callSuper = true) + public static class ImageGraphic extends LabelGraphic { + + String file; + int size; + + @Override + public Node createGraphicNode() { + return PrettyImageHelper.ofFixedSizeSquare(file, size).createRegion(); + } + } + @Value @EqualsAndHashCode(callSuper = true) public static class CompGraphic extends LabelGraphic { diff --git a/app/src/main/java/io/xpipe/app/fxcomps/util/PlatformThread.java b/app/src/main/java/io/xpipe/app/fxcomps/util/PlatformThread.java index 6ffc3d887..c05a554bc 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/util/PlatformThread.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/util/PlatformThread.java @@ -3,6 +3,7 @@ package io.xpipe.app.fxcomps.util; import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.util.PlatformState; + import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.Observable; @@ -11,6 +12,7 @@ import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; + import lombok.NonNull; import java.util.*; 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 b6c983abe..210afe012 100644 --- a/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java +++ b/app/src/main/java/io/xpipe/app/issue/ErrorEvent.java @@ -57,7 +57,7 @@ public class ErrorEvent { return EVENT_BASES.remove(t).description(msg); } - return builder().throwable(t).description(msg + (t.getMessage() != null ? "\n\n" + t.getMessage() : "")); + return builder().throwable(t).description(msg + (t.getMessage() != null ? "\n\n" + t.getMessage().trim() : "")); } public static ErrorEventBuilder fromMessage(String msg) { 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 9f5caae2a..d5054accc 100644 --- a/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java +++ b/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java @@ -196,6 +196,7 @@ public class ErrorHandlerComp extends SimpleComp { if (desc == null) { desc = AppI18n.get("errorNoDetail"); } + desc = desc.trim(); var graphic = new FontIcon("mdomz-warning"); graphic.setIconColor(Color.RED); @@ -204,7 +205,7 @@ public class ErrorHandlerComp extends SimpleComp { header.setGraphicTextGap(6); AppFont.setSize(header, 3); var descriptionField = new TextArea(desc); - descriptionField.setPrefRowCount(6); + descriptionField.setPrefRowCount(Math.max(5, Math.min((int) desc.lines().count(), 14))); descriptionField.setWrapText(true); descriptionField.setEditable(false); descriptionField.setPadding(Insets.EMPTY); 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 9099461b9..3359ce6cb 100644 --- a/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java +++ b/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java @@ -82,7 +82,7 @@ public class SentryErrorHandler implements ErrorHandler { causeField.set(copy, adjustCopy(throwable.getCause(), true)); return copy; - } catch (Exception e) { + } catch (Throwable e) { // This can fail for example when the underlying exception is not serializable // and comes from some third party library if (AppLogs.get() != null) { 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 689125423..3c4d9f8f7 100644 --- a/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java +++ b/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java @@ -98,9 +98,11 @@ public class LauncherCommand implements Callable { } } catch (Exception ex) { var cli = XPipeInstallation.getLocalDefaultCliExecutable(); - ErrorEvent.fromThrowable("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.", ex) + ErrorEvent.fromThrowable( + "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.", + ex) .term() .expected() .handle(); @@ -127,9 +129,16 @@ public class LauncherCommand implements Callable { // there might be another instance running, for example // starting up or listening on another port if (!AppDataLock.lock()) { - TrackEvent.info( - "Data directory " + AppProperties.get().getDataDir().toString() - + " is already locked. Is another instance running?"); + TrackEvent.info("Data directory " + AppProperties.get().getDataDir().toString() + + " is already locked. Is another instance running?"); + OperationMode.halt(1); + } + + // If an instance is running as another user, we cannot connect to it as the xpipe_auth file is inaccessible + // Therefore the beacon client is not present. + // We still should check whether it is somehow occupied, otherwise beacon server startup will fail + if (BeaconClient.isOccupied(port)) { + TrackEvent.info("Another instance is already running on this port as another user. Quitting ..."); OperationMode.halt(1); } } 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 3e94b72b9..c54aaff58 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java @@ -14,18 +14,16 @@ import io.xpipe.app.terminal.ExternalTerminalType; import io.xpipe.app.util.PasswordLockSecretValue; import io.xpipe.core.util.InPlaceSecretValue; import io.xpipe.core.util.ModuleHelper; - 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 lombok.Getter; import lombok.Value; +import org.apache.commons.io.FileUtils; -import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.stream.Stream; @@ -33,7 +31,7 @@ import java.util.stream.Stream; public class AppPrefs { public static final Path DEFAULT_STORAGE_DIR = - AppProperties.get().getDataDir().resolve("storage"); + AppProperties.get() != null ? AppProperties.get().getDataDir().resolve("storage") : null; private static final String DEVELOPER_MODE_PROP = "io.xpipe.app.developerMode"; private static AppPrefs INSTANCE; private final List> mapping = new ArrayList<>(); @@ -111,6 +109,9 @@ public class AppPrefs { map(new SimpleBooleanProperty(false), "developerDisableGuiRestrictions", Boolean.class); private final ObservableBooleanValue developerDisableGuiRestrictionsEffective = bindDeveloperTrue(developerDisableGuiRestrictions); + final BooleanProperty developerForceSshTty = + map(new SimpleBooleanProperty(false), "developerForceSshTty", Boolean.class); + final ObjectProperty language = map(new SimpleObjectProperty<>(SupportedLocale.getEnglish()), "language", SupportedLocale.class); @@ -175,6 +176,7 @@ public class AppPrefs { new SecurityCategory(), new HttpApiCategory(), new WorkflowCategory(), + new WorkspacesCategory(), new TroubleshootCategory(), new DeveloperCategory()) .filter(appPrefsCategory -> appPrefsCategory.show()) @@ -436,6 +438,10 @@ public class AppPrefs { return developerDisableGuiRestrictionsEffective; } + public ObservableBooleanValue developerForceSshTty() { + return bindDeveloperTrue(developerForceSshTty); + } + @SuppressWarnings("unchecked") private T map(T o, String name, Class clazz) { mapping.add(new Mapping<>(name, (Property) o, (Class) clazz)); @@ -489,7 +495,7 @@ public class AppPrefs { } try { - Files.createDirectories(storageDirectory.get()); + FileUtils.forceMkdir(storageDirectory.getValue().toFile()); } catch (Exception e) { ErrorEvent.fromThrowable(e).expected().build().handle(); storageDirectory.setValue(DEFAULT_STORAGE_DIR); diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java index bd8ea3c1f..26332908a 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefsComp.java @@ -41,9 +41,9 @@ public class AppPrefsComp extends SimpleComp { pfxLimit.setAlignment(Pos.TOP_LEFT); var sidebar = new AppPrefsSidebarComp().createRegion(); - sidebar.setMinWidth(350); - sidebar.setPrefWidth(350); - sidebar.setMaxWidth(350); + sidebar.setMinWidth(280); + sidebar.setPrefWidth(280); + sidebar.setMaxWidth(280); var split = new HBox(sidebar, pfxLimit); HBox.setHgrow(pfxLimit, Priority.ALWAYS); 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 3876d4cf4..11dde276b 100644 --- a/app/src/main/java/io/xpipe/app/prefs/DeveloperCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/DeveloperCategory.java @@ -61,6 +61,8 @@ public class DeveloperCategory extends AppPrefsCategory { .sub(new OptionsBuilder() .nameAndDescription("developerDisableUpdateVersionCheck") .addToggle(prefs.developerDisableUpdateVersionCheck) + .nameAndDescription("developerForceSshTty") + .addToggle(prefs.developerForceSshTty) .nameAndDescription("developerDisableGuiRestrictions") .addToggle(prefs.developerDisableGuiRestrictions) .nameAndDescription("shellCommandTest") 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 82153394e..c32dd4254 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java @@ -114,14 +114,12 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue { protected Optional determineFromPath() { // Try to locate if it is in the Path - try (var cc = LocalShell.getShell() - .command(CommandBuilder.ofFunction( - var1 -> var1.getShellDialect().getWhichCommand(executable))) + try (var sc = LocalShell.getShell() .start()) { - var out = cc.readStdoutDiscardErr(); - var exit = cc.getExitCode(); - if (exit == 0) { - var first = out.lines().findFirst(); + var out = sc.command(CommandBuilder.ofFunction( + var1 -> var1.getShellDialect().getWhichCommand(executable))).readStdoutIfPossible(); + if (out.isPresent()) { + var first = out.get().lines().findFirst(); if (first.isPresent()) { return first.map(String::trim).map(Path::of); } 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 52ee01170..c5a3d5b74 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java @@ -78,6 +78,10 @@ public interface ExternalEditorType extends PrefsChoiceValue { LinuxPathType VSCODE_LINUX = new LinuxPathType("app.vscode", "code"); + LinuxPathType ZED_LINUX = new LinuxPathType("app.zed", "zed"); + + ExternalEditorType ZED_MACOS = new MacOsEditor("app.zed", "Zed"); + LinuxPathType VSCODIUM_LINUX = new LinuxPathType("app.vscodium", "codium"); LinuxPathType GNOME = new LinuxPathType("app.gnomeTextEditor", "gnome-text-editor"); @@ -124,8 +128,9 @@ public interface ExternalEditorType extends PrefsChoiceValue { 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.of(VSCODIUM_LINUX, VSCODE_LINUX, ZED_LINUX, KATE, GEDIT, PLUMA, LEAFPAD, MOUSEPAD, GNOME); + List MACOS_EDITORS = + List.of(BBEDIT, VSCODIUM_MACOS, VSCODE_MACOS, SUBLIME_MACOS, ZED_MACOS, TEXT_EDIT); List CROSS_PLATFORM_EDITORS = List.of(FLEET, INTELLIJ, PYCHARM, WEBSTORM, CLION); @SuppressWarnings("TrivialFunctionalExpressionUsage") diff --git a/app/src/main/java/io/xpipe/app/prefs/SyncCategory.java b/app/src/main/java/io/xpipe/app/prefs/SyncCategory.java index 407c38928..da3bf2c0f 100644 --- a/app/src/main/java/io/xpipe/app/prefs/SyncCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/SyncCategory.java @@ -2,7 +2,6 @@ package io.xpipe.app.prefs; import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.core.AppI18n; -import io.xpipe.app.core.AppProperties; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.util.DesktopHelper; @@ -21,10 +20,8 @@ public class SyncCategory extends AppPrefsCategory { builder.addTitle("sync") .sub(new OptionsBuilder() .name("enableGitStorage") - .description( - AppProperties.get().isStaging() && !prefs.developerMode().getValue() ? "enableGitStoragePtbDisabled" : "enableGitStorage") + .description("enableGitStorageDescription") .addToggle(prefs.enableGitStorage) - .disable(AppProperties.get().isStaging() && !prefs.developerMode().getValue()) .nameAndDescription("storageGitRemote") .addString(prefs.storageGitRemote, true) .disable(prefs.enableGitStorage.not()) 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 86e98c7d4..cba1d6179 100644 --- a/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java @@ -36,8 +36,6 @@ public class VaultCategory extends AppPrefsCategory { } builder.addTitle("vaultSecurity") .sub(new OptionsBuilder() - .nameAndDescription("encryptAllVaultData") - .addToggle(prefs.encryptAllVaultData) .nameAndDescription("workspaceLock") .addComp( new ButtonComp( @@ -57,7 +55,9 @@ public class VaultCategory extends AppPrefsCategory { .addToggle(prefs.lockVaultOnHibernation) .hide(prefs.getLockCrypt() .isNull() - .or(prefs.getLockCrypt().isEmpty()))); + .or(prefs.getLockCrypt().isEmpty())) + .nameAndDescription("encryptAllVaultData") + .addToggle(prefs.encryptAllVaultData)); return builder.buildComp(); } } diff --git a/app/src/main/java/io/xpipe/app/prefs/WorkspaceCreationAlert.java b/app/src/main/java/io/xpipe/app/prefs/WorkspaceCreationAlert.java new file mode 100644 index 000000000..3ff605f35 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/WorkspaceCreationAlert.java @@ -0,0 +1,75 @@ +package io.xpipe.app.prefs; + +import io.xpipe.app.core.AppFont; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.core.AppProperties; +import io.xpipe.app.core.mode.OperationMode; +import io.xpipe.app.core.window.AppWindowHelper; +import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.util.DesktopHelper; +import io.xpipe.app.util.DesktopShortcuts; +import io.xpipe.app.util.OptionsBuilder; +import io.xpipe.app.util.ThreadHelper; +import io.xpipe.core.process.OsType; +import io.xpipe.core.util.XPipeInstallation; +import javafx.beans.property.SimpleObjectProperty; +import javafx.geometry.Insets; +import javafx.scene.control.ButtonType; +import org.apache.commons.io.FileUtils; + +import java.nio.file.Files; + +public class WorkspaceCreationAlert { + + public static void showAsync() { + ThreadHelper.runFailableAsync(() -> { + show(); + }); + } + + private static void show() throws Exception { + var name = new SimpleObjectProperty<>("New workspace"); + var path = new SimpleObjectProperty<>(AppProperties.get().getDataDir()); + var show = AppWindowHelper.showBlockingAlert(alert -> { + alert.setTitle(AppI18n.get("workspaceCreationAlertTitle")); + var content = new OptionsBuilder() + .nameAndDescription("workspaceName") + .addString(name) + .nameAndDescription("workspacePath") + .addPath(path) + .buildComp() + .minWidth(500) + .padding(new Insets(5, 20, 20, 20)) + .apply(struc -> AppFont.small(struc.get())) + .createRegion(); + alert.getButtonTypes().add(ButtonType.CANCEL); + alert.getButtonTypes().add(ButtonType.OK); + alert.getDialogPane().setContent(content); + }) + .map(b -> b.getButtonData().isDefaultButton()) + .orElse(false); + + if (!show || name.get() == null || path.get() == null) { + return; + } + + if (Files.exists(path.get()) && !FileUtils.isEmptyDirectory(path.get().toFile())) { + ErrorEvent.fromMessage("New workspace directory is not empty").expected().handle(); + return; + } + + var shortcutName = (AppProperties.get().isStaging() ? "XPipe PTB" : "XPipe") + " (" + name.get() + ")"; + var file = switch (OsType.getLocal()) { + case OsType.Windows w -> { + var exec = XPipeInstallation.getCurrentInstallationBasePath().resolve(XPipeInstallation.getDaemonExecutablePath(w)).toString(); + yield DesktopShortcuts.create(exec, "-Dio.xpipe.app.dataDir=\"" + path.get().toString() + "\" -Dio.xpipe.app.acceptEula=true", shortcutName); + } + default -> { + var exec = XPipeInstallation.getCurrentInstallationBasePath().resolve(XPipeInstallation.getRelativeCliExecutablePath(OsType.getLocal())).toString(); + yield DesktopShortcuts.create(exec, "-d \"" + path.get().toString() + "\" --accept-eula", shortcutName); + } + }; + DesktopHelper.browseFileInDirectory(file); + OperationMode.close(); + } +} diff --git a/app/src/main/java/io/xpipe/app/prefs/WorkspacesCategory.java b/app/src/main/java/io/xpipe/app/prefs/WorkspacesCategory.java new file mode 100644 index 000000000..2c186e008 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/WorkspacesCategory.java @@ -0,0 +1,26 @@ +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.util.OptionsBuilder; + +public class WorkspacesCategory extends AppPrefsCategory { + + @Override + protected String getId() { + return "workspaces"; + } + + @Override + protected Comp create() { + return new OptionsBuilder() + .addTitle("manageWorkspaces") + .sub(new OptionsBuilder() + .nameAndDescription("workspaceAdd") + .addComp( + new ButtonComp(AppI18n.observable("addWorkspace"), + WorkspaceCreationAlert::showAsync))) + .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 063d4c8e3..e12bb513b 100644 --- a/app/src/main/java/io/xpipe/app/storage/ContextualFileReference.java +++ b/app/src/main/java/io/xpipe/app/storage/ContextualFileReference.java @@ -23,7 +23,7 @@ public class ContextualFileReference { private static String getDataDir() { if (DataStorage.get() == null) { - return lastDataDir != null ? lastDataDir : normalized(AppPrefs.DEFAULT_STORAGE_DIR); + return lastDataDir != null ? lastDataDir : normalized(AppPrefs.DEFAULT_STORAGE_DIR.resolve("data")); } return lastDataDir = normalized(DataStorage.get().getDataDir()); 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 2573e747c..4cc3d1106 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java @@ -273,6 +273,13 @@ public abstract class DataStorage { } public void updateEntry(DataStoreEntry entry, DataStoreEntry newEntry) { + var state = entry.getStorePersistentState(); + var nState = newEntry.getStorePersistentState(); + if (state != null && nState != null) { + var updatedState = state.mergeCopy(nState); + newEntry.setStorePersistentState(updatedState); + } + var oldParent = DataStorage.get().getDefaultDisplayParent(entry); var newParent = DataStorage.get().getDefaultDisplayParent(newEntry); var sameParent = Objects.equals(oldParent, newParent); @@ -341,18 +348,20 @@ public abstract class DataStorage { @SneakyThrows public boolean refreshChildren(DataStoreEntry e) { - return refreshChildren(e,false); + return refreshChildren(e, false); } public boolean refreshChildren(DataStoreEntry e, boolean throwOnFail) throws Exception { - if (!(e.getStore() instanceof FixedHierarchyStore)) { + if (!(e.getStore() instanceof FixedHierarchyStore h)) { return false; } e.incrementBusyCounter(); List> newChildren; try { - newChildren = ((FixedHierarchyStore) (e.getStore())).listChildren(e).stream().filter(dataStoreEntryRef -> dataStoreEntryRef != null && dataStoreEntryRef.get() != null).toList(); + newChildren = h.listChildren(e).stream() + .filter(dataStoreEntryRef -> dataStoreEntryRef != null && dataStoreEntryRef.get() != null) + .toList(); } catch (Exception ex) { if (throwOnFail) { throw ex; @@ -368,6 +377,10 @@ public abstract class DataStorage { var toRemove = oldChildren.stream() .filter(oc -> oc.getStore() instanceof FixedChildStore) .filter(oc -> { + if (!oc.getValidity().isUsable()) { + return true; + } + var oid = ((FixedChildStore) oc.getStore()).getFixedId(); if (oid.isEmpty()) { return false; @@ -394,6 +407,7 @@ public abstract class DataStorage { return oldChildren.stream() .filter(oc -> oc.getStore() instanceof FixedChildStore) + .filter(oc -> oc.getValidity().isUsable()) .filter(oc -> ((FixedChildStore) oc.getStore()) .getFixedId() .isPresent()) @@ -407,6 +421,7 @@ public abstract class DataStorage { .toList(); var toUpdate = oldChildren.stream() .filter(oc -> oc.getStore() instanceof FixedChildStore) + .filter(oc -> oc.getValidity().isUsable()) .map(oc -> { var oid = ((FixedChildStore) oc.getStore()).getFixedId(); if (oid.isEmpty()) { @@ -433,7 +448,9 @@ public abstract class DataStorage { nc.get().notifyUpdate(false, true); }); - deleteWithChildren(toRemove.toArray(DataStoreEntry[]::new)); + if (h.removeLeftovers()) { + deleteWithChildren(toRemove.toArray(DataStoreEntry[]::new)); + } addStoreEntriesIfNotPresent(toAdd.stream().map(DataStoreEntryRef::get).toArray(DataStoreEntry[]::new)); toUpdate.forEach(pair -> { // Update state by merging @@ -460,10 +477,11 @@ public abstract class DataStorage { }); refreshEntries(); saveAsync(); + e.getProvider().onChildrenRefresh(e); toAdd.forEach(dataStoreEntryRef -> - dataStoreEntryRef.get().getProvider().onChildrenRefresh(dataStoreEntryRef.getEntry())); + dataStoreEntryRef.get().getProvider().onParentRefresh(dataStoreEntryRef.getEntry())); toUpdate.forEach(dataStoreEntryRef -> - dataStoreEntryRef.getKey().getProvider().onChildrenRefresh(dataStoreEntryRef.getKey())); + dataStoreEntryRef.getKey().getProvider().onParentRefresh(dataStoreEntryRef.getKey())); return !newChildren.isEmpty(); } @@ -865,24 +883,16 @@ public abstract class DataStorage { .findFirst(); } - public Optional getStoreDisplayName(DataStore store) { - if (store == null) { - return Optional.empty(); - } - - return getStoreEntryIfPresent(store, true).map(dataStoreEntry -> dataStoreEntry.getName()); - } - - public String getStoreDisplayName(DataStoreEntry store) { - if (store == null) { + public String getStoreEntryDisplayName(DataStoreEntry entry) { + if (entry == null) { return "?"; } - if (!store.getValidity().isUsable()) { + if (!entry.getValidity().isUsable()) { return "?"; } - return store.getProvider().browserDisplayName(store.getStore()); + return entry.getProvider().displayName(entry); } public Optional getStoreEntryIfPresent(UUID id) { 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 35ceb6fee..9e108efcf 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java @@ -101,9 +101,7 @@ public class DataStoreEntry extends StorageElement { this.expanded = expanded; this.color = color; this.explicitOrder = explicitOrder; - this.provider = store != null - ? DataStoreProviders.byStore(store) - : null; + this.provider = store != null ? DataStoreProviders.byStore(store) : null; this.storePersistentStateNode = storePersistentState; this.notes = notes; } @@ -159,7 +157,7 @@ public class DataStoreEntry extends StorageElement { null, uuid, categoryUuid, - name, + name.trim(), Instant.now(), Instant.now(), storeFromNode, @@ -196,7 +194,7 @@ public class DataStoreEntry extends StorageElement { var categoryUuid = Optional.ofNullable(json.get("categoryUuid")) .map(jsonNode -> UUID.fromString(jsonNode.textValue())) .orElse(DataStorage.DEFAULT_CATEGORY_UUID); - var name = json.required("name").textValue(); + var name = json.required("name").textValue().trim(); var persistentState = stateJson.get("persistentState"); var lastUsed = Optional.ofNullable(stateJson.get("lastUsed")) @@ -259,7 +257,7 @@ public class DataStoreEntry extends StorageElement { store, storeNode, false, - Validity.INCOMPLETE, + store == null ? Validity.LOAD_FAILED : Validity.INCOMPLETE, configuration, persistentState, expanded, @@ -452,6 +450,7 @@ public class DataStoreEntry extends StorageElement { this.store = store; this.storeNode = JacksonMapper.getDefault().valueToTree(store); + this.provider = DataStoreProviders.byStore(store); if (updateTime) { lastModified = Instant.now(); } @@ -500,6 +499,8 @@ public class DataStoreEntry extends StorageElement { DataStore newStore; try { newStore = JacksonMapper.getDefault().treeToValue(storeNode, DataStore.class); + // Check whether we have a provider as well + DataStoreProviders.byStore(newStore); } catch (Throwable e) { ErrorEvent.fromThrowable(e).handle(); newStore = null; 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 0a2d46f4a..9191c2f91 100644 --- a/app/src/main/java/io/xpipe/app/storage/StandardStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/StandardStorage.java @@ -1,14 +1,14 @@ package io.xpipe.app.storage; -import com.fasterxml.jackson.core.JacksonException; import io.xpipe.app.ext.DataStorageExtensionProvider; 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 io.xpipe.core.util.JacksonMapper; + +import com.fasterxml.jackson.core.JacksonException; import lombok.Getter; import org.apache.commons.io.FileUtils; @@ -53,19 +53,27 @@ public class StandardStorage extends DataStorage { try { FileUtils.forceMkdir(dir.toFile()); } catch (Exception e) { - ErrorEvent.fromThrowable("Unable to create vault directory", e).terminal(true).build().handle(); + ErrorEvent.fromThrowable("Unable to create vault directory", e) + .terminal(true) + .build() + .handle(); } try { initSystemInfo(); } catch (Exception e) { - ErrorEvent.fromThrowable("Unable to load vault system info", e).build().handle(); + ErrorEvent.fromThrowable("Unable to load vault system info", e) + .build() + .handle(); } try { initVaultKey(); } catch (Exception e) { - ErrorEvent.fromThrowable("Unable to load vault key file", e).terminal(true).build().handle(); + ErrorEvent.fromThrowable("Unable to load vault key file", e) + .terminal(true) + .build() + .handle(); } var storesDir = getStoresDir(); @@ -76,7 +84,10 @@ public class StandardStorage extends DataStorage { FileUtils.forceMkdir(categoriesDir.toFile()); FileUtils.forceMkdir(dataDir.toFile()); } catch (Exception e) { - ErrorEvent.fromThrowable("Unable to create vault directory", e).terminal(true).build().handle(); + ErrorEvent.fromThrowable("Unable to create vault directory", e) + .terminal(true) + .build() + .handle(); } try { @@ -215,17 +226,26 @@ public class StandardStorage extends DataStorage { local.setColor(DataStoreColor.BLUE); } - callProviders(); + // Reload stores, this time with all entry refs present + // These do however not have a completed validity yet refreshEntries(); + // Bring entries into completed validity if possible + // Needed for chained stores + refreshEntries(); + // Let providers work on complete stores + callProviders(); + // Update validaties after any possible changes + refreshEntries(); + // Add any possible missing synthetic parents storeEntriesSet.forEach(entry -> { var syntheticParent = getSyntheticParent(entry); syntheticParent.ifPresent(entry1 -> { addStoreEntryIfNotPresent(entry1); }); }); + // Update validaties from synthetic parent I changes refreshEntries(); - // Save to apply changes if (!hasFixedLocal) { storeEntriesSet.removeIf(dataStoreEntry -> !dataStoreEntry.getUuid().equals(LOCAL_ID) && dataStoreEntry.getStore() instanceof LocalStore); @@ -235,6 +255,7 @@ public class StandardStorage extends DataStorage { entry.dirty = true; entry.setStoreNode(JacksonMapper.getDefault().valueToTree(entry.getStore())); }); + // Save to apply changes save(false); } @@ -416,7 +437,7 @@ public class StandardStorage extends DataStorage { var s = Files.readString(file); vaultKey = new String(Base64.getDecoder().decode(s), StandardCharsets.UTF_8); } else { - Files.createDirectories(dir); + FileUtils.forceMkdir(dir.toFile()); vaultKey = UUID.randomUUID().toString(); Files.writeString(file, Base64.getEncoder().encodeToString(vaultKey.getBytes(StandardCharsets.UTF_8))); } @@ -437,7 +458,7 @@ public class StandardStorage extends DataStorage { Files.writeString(file, s); } } else { - Files.createDirectories(dir); + FileUtils.forceMkdir(dir.toFile()); var s = OsType.getLocal().getName(); Files.writeString(file, s); } 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 27fa9550b..9e5f03064 100644 --- a/app/src/main/java/io/xpipe/app/update/AppDownloads.java +++ b/app/src/main/java/io/xpipe/app/update/AppDownloads.java @@ -91,7 +91,7 @@ public class AppDownloads { var changelog = json.required("changelog").asText(); return Optional.of(changelog); } catch (Throwable t) { - ErrorEvent.fromThrowable(t).omit().handle(); + ErrorEvent.fromThrowable(t).omit().expected().handle(); } try { 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 3a4497ca0..065904c28 100644 --- a/app/src/main/java/io/xpipe/app/update/AppInstaller.java +++ b/app/src/main/java/io/xpipe/app/update/AppInstaller.java @@ -2,13 +2,17 @@ package io.xpipe.app.update; import io.xpipe.app.core.AppLogs; import io.xpipe.app.core.AppProperties; +import io.xpipe.app.core.mode.OperationMode; +import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.LocalShell; import io.xpipe.app.util.ScriptHelper; import io.xpipe.app.util.TerminalLauncher; +import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.process.OsType; import io.xpipe.core.process.ShellDialects; import io.xpipe.core.store.FileNames; import io.xpipe.core.store.LocalStore; +import io.xpipe.core.util.FailableRunnable; import io.xpipe.core.util.XPipeInstallation; import com.fasterxml.jackson.annotation.JsonSubTypes; @@ -21,10 +25,6 @@ import java.nio.file.Path; public class AppInstaller { - public static void installFileLocal(InstallerAssetType asset, Path localFile) throws Exception { - asset.installLocal(localFile.toString()); - } - public static InstallerAssetType getSuitablePlatformAsset() { if (OsType.getLocal().equals(OsType.WINDOWS)) { return new InstallerAssetType.Msi(); @@ -53,7 +53,18 @@ public class AppInstaller { }) public abstract static class InstallerAssetType { - public abstract void installLocal(String file) throws Exception; + protected void runAndClose(FailableRunnable r) { + OperationMode.executeAfterShutdown(() -> { + r.run(); + + // 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); + }); + } + + public abstract void installLocal(Path file) throws Exception; public boolean isCorrectAsset(String name) { return name.endsWith(getExtension()) @@ -66,7 +77,7 @@ public class AppInstaller { public static final class Msi extends InstallerAssetType { @Override - public void installLocal(String file) throws Exception { + public void installLocal(Path file) throws Exception { var shellProcessControl = new LocalStore().control().start(); var exec = (AppProperties.get().isDevelopmentEnvironment() ? Path.of(XPipeInstallation.getLocalDefaultInstallationBasePath()) @@ -75,15 +86,19 @@ public class AppInstaller { .toString(); var logsDir = AppLogs.get().getSessionLogsDirectory().getParent().toString(); - var logFile = FileNames.join(logsDir, "installer_" + FileNames.getFileName(file) + ".log"); + var logFile = FileNames.join( + logsDir, "installer_" + file.getFileName().toString() + ".log"); var command = LocalShell.getShell().getShellDialect().equals(ShellDialects.CMD) - ? getCmdCommand(file, logFile, exec) - : getPowershellCommand(file, logFile, exec); + ? getCmdCommand(file.toString(), logFile, exec) + : getPowershellCommand(file.toString(), logFile, exec); var toRun = LocalShell.getShell().getShellDialect().equals(ShellDialects.CMD) ? "start \"XPipe Updater\" /min cmd /c \"" + ScriptHelper.createLocalExecScript(command) + "\"" : "Start-Process -WindowStyle Minimized -FilePath powershell -ArgumentList \"-ExecutionPolicy\", \"Bypass\", \"-File\", \"`\"" + ScriptHelper.createLocalExecScript(command) + "`\"\""; - shellProcessControl.executeSimpleCommand(toRun); + + runAndClose(() -> { + shellProcessControl.executeSimpleCommand(toRun); + }); } @Override @@ -124,7 +139,14 @@ public class AppInstaller { public static final class Debian extends InstallerAssetType { @Override - public void installLocal(String file) throws Exception { + public void installLocal(Path file) throws Exception { + var start = AppPrefs.get() != null + && AppPrefs.get().terminalType().getValue() != null + && AppPrefs.get().terminalType().getValue().isAvailable(); + if (!start) { + return; + } + var name = AppProperties.get().isStaging() ? "xpipe-ptb" : "xpipe"; var command = String.format( """ @@ -139,7 +161,10 @@ public class AppInstaller { exec || read -rsp "Update failed ..."$'\\n' -n 1 key """, file, file, name); - TerminalLauncher.openDirect("XPipe Updater", sc -> command); + + runAndClose(() -> { + TerminalLauncher.openDirect("XPipe Updater", sc -> command); + }); } @Override @@ -150,8 +175,16 @@ public class AppInstaller { @JsonTypeName("rpm") public static final class Rpm extends InstallerAssetType { + @Override - public void installLocal(String file) throws Exception { + public void installLocal(Path file) throws Exception { + var start = AppPrefs.get() != null + && AppPrefs.get().terminalType().getValue() != null + && AppPrefs.get().terminalType().getValue().isAvailable(); + if (!start) { + return; + } + var name = AppProperties.get().isStaging() ? "xpipe-ptb" : "xpipe"; var command = String.format( """ @@ -166,7 +199,10 @@ public class AppInstaller { exec || read -rsp "Update failed ..."$'\\n' -n 1 key """, file, file, name); - TerminalLauncher.openDirect("XPipe Updater", sc -> command); + + runAndClose(() -> { + TerminalLauncher.openDirect("XPipe Updater", sc -> command); + }); } @Override @@ -177,8 +213,16 @@ public class AppInstaller { @JsonTypeName("pkg") public static final class Pkg extends InstallerAssetType { + @Override - public void installLocal(String file) throws Exception { + public void installLocal(Path file) throws Exception { + var start = AppPrefs.get() != null + && AppPrefs.get().terminalType().getValue() != null + && AppPrefs.get().terminalType().getValue().isAvailable(); + if (!start) { + return; + } + var name = AppProperties.get().isStaging() ? "xpipe-ptb" : "xpipe"; var command = String.format( """ @@ -193,7 +237,10 @@ public class AppInstaller { exec || echo "Update failed ..." && read -rs -k 1 key """, file, file, name); - TerminalLauncher.openDirect("XPipe Updater", sc -> command); + + runAndClose(() -> { + TerminalLauncher.openDirect("XPipe Updater", sc -> command); + }); } @Override 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 77fd593f3..e256053f1 100644 --- a/app/src/main/java/io/xpipe/app/update/GitHubUpdater.java +++ b/app/src/main/java/io/xpipe/app/update/GitHubUpdater.java @@ -1,6 +1,8 @@ package io.xpipe.app.update; +import io.xpipe.app.core.AppCache; import io.xpipe.app.core.AppProperties; +import io.xpipe.app.issue.ErrorEvent; import javafx.scene.layout.Region; @@ -43,13 +45,24 @@ public class GitHubUpdater extends UpdateHandler { preparedUpdate.setValue(rel); } - public void executeUpdateOnCloseImpl() throws Exception { - var downloadFile = preparedUpdate.getValue().getFile(); + public void executeUpdate() { + var p = preparedUpdate.getValue(); + var downloadFile = p.getFile(); if (!Files.exists(downloadFile)) { + event("Prepared update file does not exist"); return; } - AppInstaller.installFileLocal(preparedUpdate.getValue().getAssetType(), downloadFile); + try { + var performedUpdate = new PerformedUpdate(p.getVersion(), p.getBody(), p.getVersion()); + AppCache.update("performedUpdate", performedUpdate); + + var a = p.getAssetType(); + a.installLocal(downloadFile); + } catch (Throwable t) { + ErrorEvent.fromThrowable(t).handle(); + preparedUpdate.setValue(null); + } } public synchronized AvailableRelease refreshUpdateCheckImpl() throws Exception { 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 26099771a..8aafadc40 100644 --- a/app/src/main/java/io/xpipe/app/update/PortableUpdater.java +++ b/app/src/main/java/io/xpipe/app/update/PortableUpdater.java @@ -29,10 +29,6 @@ public class PortableUpdater extends UpdateHandler { .createRegion(); } - public void executeUpdateOnCloseImpl() { - throw new UnsupportedOperationException(); - } - public synchronized AvailableRelease refreshUpdateCheckImpl() throws Exception { var rel = AppDownloads.getLatestSuitableRelease(); event("Determined latest suitable release " 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 09b36c502..82f1ae293 100644 --- a/app/src/main/java/io/xpipe/app/update/UpdateHandler.java +++ b/app/src/main/java/io/xpipe/app/update/UpdateHandler.java @@ -2,7 +2,6 @@ package io.xpipe.app.update; import io.xpipe.app.core.AppCache; import io.xpipe.app.core.AppProperties; -import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.prefs.AppPrefs; @@ -72,6 +71,13 @@ public abstract class UpdateHandler { preparedUpdate.setValue(null); } + // Check if file has been deleted + if (preparedUpdate.getValue() != null + && preparedUpdate.getValue().getFile() != null + && !Files.exists(preparedUpdate.getValue().getFile())) { + preparedUpdate.setValue(null); + } + preparedUpdate.addListener((c, o, n) -> { AppCache.update("preparedUpdate", n); }); @@ -220,27 +226,10 @@ public abstract class UpdateHandler { } event("Executing update ..."); - OperationMode.executeAfterShutdown(() -> { - try { - var performedUpdate = new PerformedUpdate( - preparedUpdate.getValue().getVersion(), - preparedUpdate.getValue().getBody(), - 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(); - } - }); + executeUpdate(); } - public void executeUpdateOnCloseImpl() throws Exception { + public void executeUpdate() { throw new UnsupportedOperationException(); } 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 410aa38a1..0d6c8d6e3 100644 --- a/app/src/main/java/io/xpipe/app/update/XPipeDistributionType.java +++ b/app/src/main/java/io/xpipe/app/update/XPipeDistributionType.java @@ -99,16 +99,13 @@ public enum XPipeDistributionType { // 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()) { - var out = chocoOut.readStdoutDiscardErr(); - if (chocoOut.getExitCode() == 0) { - var split = out.split("\\|"); - if (split.length == 2) { - var version = split[1]; - if (AppProperties.get().getVersion().equals(version)) { - return CHOCO; - } + var out = sc.command("choco search --local-only -r xpipe").readStdoutIfPossible(); + if (out.isPresent()) { + var split = out.get().split("\\|"); + if (split.length == 2) { + var version = split[1]; + if (AppProperties.get().getVersion().equals(version)) { + return CHOCO; } } } @@ -117,17 +114,15 @@ public enum XPipeDistributionType { // 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(); - if (brewOut.getExitCode() == 0) { - if (out.lines().anyMatch(s -> { - var split = s.split(" "); - return split.length == 2 - && split[0].equals("xpipe") - && split[1].equals(AppProperties.get().getVersion()); - })) { - return HOMEBREW; - } + var out = sc.command("brew list --casks --versions").readStdoutIfPossible(); + if (out.isPresent()) { + if (out.get().lines().anyMatch(s -> { + var split = s.split(" "); + return split.length == 2 + && split[0].equals("xpipe") + && split[1].equals(AppProperties.get().getVersion()); + })) { + return HOMEBREW; } } } diff --git a/app/src/main/java/io/xpipe/app/util/AppJacksonModule.java b/app/src/main/java/io/xpipe/app/util/AppJacksonModule.java index a47fd4d34..f8493ff42 100644 --- a/app/src/main/java/io/xpipe/app/util/AppJacksonModule.java +++ b/app/src/main/java/io/xpipe/app/util/AppJacksonModule.java @@ -171,7 +171,11 @@ public class AppJacksonModule extends SimpleModule { // Compatibility fix for legacy local stores var toUse = e.getStore() instanceof LocalStore ? DataStorage.get().local() : e; - return toUse != null ? new DataStoreEntryRef<>(toUse) : null; + if (toUse == null) { + return null; + } + + return new DataStoreEntryRef<>(toUse); } } } diff --git a/app/src/main/java/io/xpipe/app/util/ContextMenuHelper.java b/app/src/main/java/io/xpipe/app/util/ContextMenuHelper.java index aad686d37..8fa5dfb6d 100644 --- a/app/src/main/java/io/xpipe/app/util/ContextMenuHelper.java +++ b/app/src/main/java/io/xpipe/app/util/ContextMenuHelper.java @@ -1,6 +1,6 @@ package io.xpipe.app.util; -import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.util.LabelGraphic; import javafx.application.Platform; import javafx.geometry.Side; @@ -36,14 +36,14 @@ public class ContextMenuHelper { return contextMenu; } - public static MenuItem item(Comp graphic, String name) { - var i = new MenuItem(name, graphic.createRegion()); + public static MenuItem item(LabelGraphic graphic, String name) { + var i = new MenuItem(name, graphic.createGraphicNode()); return i; } public static void toggleShow(ContextMenu contextMenu, Node ref, Side side) { if (!contextMenu.isShowing()) { - contextMenu.show(ref, Side.RIGHT, 0, 0); + contextMenu.show(ref, side, 0, 0); } else { contextMenu.hide(); } 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 efbaf5757..5f1cb2782 100644 --- a/app/src/main/java/io/xpipe/app/util/DataStoreFormatter.java +++ b/app/src/main/java/io/xpipe/app/util/DataStoreFormatter.java @@ -2,17 +2,13 @@ package io.xpipe.app.util; import io.xpipe.app.comp.store.StoreEntryWrapper; import io.xpipe.app.fxcomps.util.BindingsHelper; -import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.core.process.ShellDialects; import io.xpipe.core.process.ShellStoreState; -import io.xpipe.core.store.DataStore; -import io.xpipe.core.store.ShellStore; +import io.xpipe.core.process.ShellTtyState; import javafx.beans.value.ObservableValue; -import java.util.function.IntFunction; - public class DataStoreFormatter { public static String formattedOsName(String osName) { @@ -46,7 +42,8 @@ public class DataStoreFormatter { return s.getShellDialect().getDisplayName(); } - return s.isRunning() ? formattedOsName(s.getOsName()) : "Connection failed"; + var prefix = s.getTtyState() != null && s.getTtyState() != ShellTtyState.NONE ? "[PTY] " : ""; + return s.isRunning() ? prefix + formattedOsName(s.getOsName()) : "Connection failed"; } return "?"; @@ -61,49 +58,10 @@ public class DataStoreFormatter { return name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase(); } - public static String formatSubHost(IntFunction func, DataStore at, int length) { - var atString = at instanceof ShellStore shellStore && !ShellStore.isLocal(shellStore) - ? DataStorage.get().getStoreDisplayName(at).orElse(null) - : null; - if (atString == null) { - return func.apply(length); - } - - var fileString = func.apply(length - atString.length() - 1); - return String.format("%s/%s", atString, fileString); - } - - public static String formatAtHost(IntFunction func, DataStore at, int length) { - var atString = at instanceof ShellStore shellStore && !ShellStore.isLocal(shellStore) - ? DataStorage.get().getStoreDisplayName(at).orElse(null) - : null; - if (atString == null) { - return func.apply(length); - } - - var fileString = func.apply(length - atString.length() - 3); - return String.format("%s @ %s", fileString, atString); - } - - public static String formatViaProxy(IntFunction func, DataStoreEntry at, int length) { - var atString = - at.getStore() instanceof ShellStore shellStore && !ShellStore.isLocal(shellStore) ? at.getName() : null; - if (atString == null) { - return func.apply(length); - } - - var fileString = func.apply(length - atString.length() - 3); - return String.format("%s > %s", atString, fileString); - } - public static String toApostropheName(DataStoreEntry input) { return toName(input, Integer.MAX_VALUE) + "'s"; } - public static String toName(DataStoreEntry input) { - return toName(input, Integer.MAX_VALUE); - } - public static String toName(DataStoreEntry input, int length) { if (input == null) { return "?"; @@ -146,7 +104,7 @@ public class DataStoreFormatter { DataStoreFormatter.cut(name, lengthShare), DataStoreFormatter.cut(region, length - lengthShare)); } - if (input.endsWith(".compute.amazonaws.com")) { + if (input.endsWith(".compute.amazonaws.com") || input.endsWith(".compute.internal")) { var split = input.split("\\."); var name = split[0]; var region = split[1]; 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 d3adaad33..190c20088 100644 --- a/app/src/main/java/io/xpipe/app/util/DesktopHelper.java +++ b/app/src/main/java/io/xpipe/app/util/DesktopHelper.java @@ -16,11 +16,10 @@ public class DesktopHelper { 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(); - var exit = cmd.getExitCode(); - if (exit == 0) { - return Path.of(read); + try (var sc = LocalShell.getShell().start()) { + var out = sc.command("xdg-user-dir DESKTOP").readStdoutIfPossible(); + if (out.isPresent()) { + return Path.of(out.get()); } } } @@ -28,6 +27,23 @@ public class DesktopHelper { return Path.of(System.getProperty("user.home") + "/Desktop"); } + public static Path getDownloadsDirectory() throws Exception { + if (OsType.getLocal() == OsType.WINDOWS) { + return Path.of(LocalShell.getLocalPowershell() + .executeSimpleStringCommand( + "(New-Object -ComObject Shell.Application).NameSpace('shell:Downloads').Self.Path")); + } else if (OsType.getLocal() == OsType.LINUX) { + try (var sc = LocalShell.getShell().start()) { + var out = sc.command("xdg-user-dir DOWNLOAD").readStdoutIfPossible(); + if (out.isPresent()) { + return Path.of(out.get()); + } + } + } + + return Path.of(System.getProperty("user.home") + "/Downloads"); + } + public static void browsePathRemote(ShellControl sc, String path, FileKind kind) throws Exception { var d = sc.getShellDialect(); switch (sc.getOsType()) { diff --git a/app/src/main/java/io/xpipe/app/util/DesktopShortcuts.java b/app/src/main/java/io/xpipe/app/util/DesktopShortcuts.java index e93f1eee1..bde39a45f 100644 --- a/app/src/main/java/io/xpipe/app/util/DesktopShortcuts.java +++ b/app/src/main/java/io/xpipe/app/util/DesktopShortcuts.java @@ -4,27 +4,31 @@ import io.xpipe.core.process.OsType; import io.xpipe.core.util.XPipeInstallation; import java.nio.file.Files; +import java.nio.file.Path; public class DesktopShortcuts { - private static void createWindowsShortcut(String target, String name) throws Exception { + private static Path createWindowsShortcut(String executable, String args, String name) throws Exception { var icon = XPipeInstallation.getLocalDefaultInstallationIcon(); - var shortcutTarget = XPipeInstallation.getLocalDefaultCliExecutable(); var shortcutPath = DesktopHelper.getDesktopDirectory().resolve(name + ".lnk"); var content = String.format( """ - set "TARGET=%s" - set "SHORTCUT=%s" - set PWS=powershell.exe -ExecutionPolicy Restricted -NoLogo -NonInteractive -NoProfile - - %%PWS%% -Command "$ws = New-Object -ComObject WScript.Shell; $s = $ws.CreateShortcut('%%SHORTCUT%%'); $S.IconLocation='%s'; $S.WindowStyle=7; $S.TargetPath = '%%TARGET%%'; $S.Arguments = 'open %s'; $S.Save()" + $TARGET="%s" + $SHORTCUT="%s" + $ws = New-Object -ComObject WScript.Shell + $s = $ws.CreateShortcut("$SHORTCUT") + $S.IconLocation='%s' + $S.WindowStyle=7 + $S.TargetPath = "$TARGET" + $S.Arguments = '%s' + $S.Save() """, - shortcutTarget, shortcutPath, icon, target); - LocalShell.getShell().executeSimpleCommand(content); + executable, shortcutPath, icon, args); + LocalShell.getLocalPowershell().executeSimpleCommand(content); + return shortcutPath; } - private static void createLinuxShortcut(String target, String name) throws Exception { - var exec = XPipeInstallation.getLocalDefaultCliExecutable(); + private static Path createLinuxShortcut(String executable, String args, String name) throws Exception { var icon = XPipeInstallation.getLocalDefaultInstallationIcon(); var content = String.format( """ @@ -32,19 +36,19 @@ public class DesktopShortcuts { Type=Application Name=%s Comment=Open with XPipe - Exec="%s" open %s + Exec="%s" %s Icon=%s Terminal=false Categories=Utility;Development; """, - name, exec, target, icon); + name, executable, args, icon); var file = DesktopHelper.getDesktopDirectory().resolve(name + ".desktop"); Files.writeString(file, content); file.toFile().setExecutable(true); + return file; } - private static void createMacOSShortcut(String target, String name) throws Exception { - var exec = XPipeInstallation.getLocalDefaultCliExecutable(); + private static Path createMacOSShortcut(String executable, String args, String name) throws Exception { var icon = XPipeInstallation.getLocalDefaultInstallationIcon(); var base = DesktopHelper.getDesktopDirectory().resolve(name + ".app"); var content = String.format( @@ -52,18 +56,18 @@ public class DesktopShortcuts { #!/usr/bin/env sh "%s" open %s """, - exec, target); + executable, args); try (var pc = LocalShell.getShell()) { pc.getShellDialect().deleteFileOrDirectory(pc, base.toString()).executeAndCheck(); pc.executeSimpleCommand(pc.getShellDialect().getMkdirsCommand(base + "/Contents/MacOS")); pc.executeSimpleCommand(pc.getShellDialect().getMkdirsCommand(base + "/Contents/Resources")); - var executable = base + "/Contents/MacOS/" + name; + var macExec = base + "/Contents/MacOS/" + name; pc.getShellDialect() - .createScriptTextFileWriteCommand(pc, content, executable) + .createScriptTextFileWriteCommand(pc, content, macExec) .execute(); - pc.executeSimpleCommand("chmod ugo+x \"" + executable + "\""); + pc.executeSimpleCommand("chmod ugo+x \"" + macExec + "\""); pc.getShellDialect() .createTextFileWriteCommand(pc, "APPL????", base + "/Contents/PkgInfo") @@ -85,15 +89,21 @@ public class DesktopShortcuts { .execute(); pc.executeSimpleCommand("cp \"" + icon + "\" \"" + base + "/Contents/Resources/icon.icns\""); } + return base; } - public static void create(String target, String name) throws Exception { + public static Path createCliOpen(String action, String name) throws Exception { + var exec = XPipeInstallation.getLocalDefaultCliExecutable(); + return create(exec, "open " + action, name); + } + + public static Path create(String executable, String args, String name) throws Exception { if (OsType.getLocal().equals(OsType.WINDOWS)) { - createWindowsShortcut(target, name); + return createWindowsShortcut(executable, args, name); } else if (OsType.getLocal().equals(OsType.LINUX)) { - createLinuxShortcut(target, name); + return createLinuxShortcut(executable, args, name); } else { - createMacOSShortcut(target, name); + return createMacOSShortcut(executable, args, name); } } } diff --git a/app/src/main/java/io/xpipe/app/util/DialogHelper.java b/app/src/main/java/io/xpipe/app/util/DialogHelper.java deleted file mode 100644 index 48a18f4b2..000000000 --- a/app/src/main/java/io/xpipe/app/util/DialogHelper.java +++ /dev/null @@ -1,116 +0,0 @@ -package io.xpipe.app.util; - -import io.xpipe.app.storage.DataStorage; -import io.xpipe.core.dialog.Dialog; -import io.xpipe.core.dialog.QueryConverter; -import io.xpipe.core.store.*; -import io.xpipe.core.util.NewLine; -import io.xpipe.core.util.SecretValue; -import io.xpipe.core.util.StreamCharset; - -import lombok.Value; - -public class DialogHelper { - - public static Dialog addressQuery(Address address) { - var hostNameQuery = Dialog.query("Hostname", false, true, false, address.getHostname(), QueryConverter.STRING); - var portQuery = Dialog.query("Port", false, true, false, address.getPort(), QueryConverter.INTEGER); - return Dialog.chain(hostNameQuery, portQuery) - .evaluateTo(() -> new Address(hostNameQuery.getResult(), portQuery.getResult())); - } - - public static Dialog machineQuery(DataStore store) { - var storeName = DataStorage.get().getStoreDisplayName(store).orElse("localhost"); - return Dialog.query("Machine", false, true, false, storeName, QueryConverter.STRING) - .map((String name) -> { - if (name.equals("local") || name.equals("localhost")) { - return new LocalStore(); - } - - var stored = DataStorage.get().getStoreEntryIfPresent(name).map(entry -> entry.getStore()); - if (stored.isEmpty()) { - throw new IllegalArgumentException(String.format("Store not found: %s", name)); - } - - if (!(stored.get() instanceof FileSystem)) { - throw new IllegalArgumentException(String.format("Store not a machine store: %s", name)); - } - - return stored.get(); - }); - } - - public static Dialog shellQuery(String displayName, DataStore store) { - var storeName = DataStorage.get().getStoreDisplayName(store).orElse("localhost"); - return Dialog.query(displayName, false, true, false, storeName, QueryConverter.STRING) - .map((String name) -> { - if (name.equals("local") || name.equals("localhost")) { - return new LocalStore(); - } - - var stored = DataStorage.get().getStoreEntryIfPresent(name).map(entry -> entry.getStore()); - if (stored.isEmpty()) { - throw new IllegalArgumentException(String.format("Store not found: %s", name)); - } - - if (!(stored.get() instanceof ShellStore)) { - throw new IllegalArgumentException(String.format("Store not a shell store: %s", name)); - } - - return stored.get(); - }); - } - - public static Dialog charsetQuery(StreamCharset c, boolean preferQuiet) { - return Dialog.query("Charset", false, true, c != null && preferQuiet, c, QueryConverter.CHARSET); - } - - public static Dialog newLineQuery(NewLine n, boolean preferQuiet) { - return Dialog.query("Newline", false, true, n != null && preferQuiet, n, QueryConverter.NEW_LINE); - } - - public static Dialog query(String desc, T value, boolean required, QueryConverter c, boolean preferQuiet) { - return Dialog.query(desc, false, required, value != null && preferQuiet, value, c); - } - - public static Dialog booleanChoice(String desc, boolean value, boolean preferQuiet) { - return Dialog.choice(desc, val -> val.toString(), true, preferQuiet, value, Boolean.TRUE, Boolean.FALSE); - } - - public static Dialog fileQuery(String name) { - return Dialog.query("File", true, true, false, name, QueryConverter.STRING); - } - - public static Dialog userQuery(String name) { - return Dialog.query("User", false, true, false, name, QueryConverter.STRING); - } - - public static Dialog namedStoreQuery(DataStore store, Class filter) { - var name = DataStorage.get().getStoreDisplayName(store).orElse(null); - return Dialog.query("Store", false, true, false, name, QueryConverter.STRING) - .map((String newName) -> { - var found = DataStorage.get() - .getStoreEntryIfPresent(newName) - .map(entry -> entry.getStore()) - .orElseThrow(); - if (!filter.isAssignableFrom(found.getClass())) { - throw new IllegalArgumentException("Incompatible store type"); - } - return found; - }); - } - - public static Dialog passwordQuery(SecretValue password) { - return Dialog.querySecret("Password", false, true, password); - } - - public static Dialog timeoutQuery(Integer timeout) { - return Dialog.query("Timeout", false, true, false, timeout, QueryConverter.INTEGER); - } - - @Value - public static class Address { - String hostname; - Integer port; - } -} 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 f726e764e..654a36c2a 100644 --- a/app/src/main/java/io/xpipe/app/util/FileBridge.java +++ b/app/src/main/java/io/xpipe/app/util/FileBridge.java @@ -94,7 +94,7 @@ public class FileBridge { 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 still no read lock after some time, just don't parse it if (!Files.exists(changed)) { event("Could not obtain read lock even after timeout. Ignoring change ..."); return; @@ -105,9 +105,8 @@ public class FileBridge { 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()) { + if (e.registerChange()) { event("Registering change for file " + TEMP.relativize(e.file) + " for editor entry " + e.getName()); - e.registerChange(); try (var in = Files.newInputStream(e.file)) { var actualSize = (long) in.available(); var started = Instant.now(); @@ -219,6 +218,7 @@ public class FileBridge { private final BooleanScope scope; private final BiConsumer writer; private Instant lastModified; + private long lastSize; public Entry(Path file, Object key, String name, BooleanScope scope, BiConsumer writer) { this.file = file; @@ -228,15 +228,6 @@ public class FileBridge { this.writer = writer; } - public boolean hasChanged() { - try { - var newDate = Files.getLastModifiedTime(file).toInstant(); - return !newDate.equals(lastModified); - } catch (IOException e) { - return false; - } - } - public Instant getLastModified() { try { return Files.getLastModifiedTime(file).toInstant(); @@ -245,8 +236,29 @@ public class FileBridge { } } - public void registerChange() { - lastModified = getLastModified(); + public long getSize() { + try { + return Files.size(file); + } catch (IOException e) { + return 0; + } + } + + public boolean registerChange() { + var newSize = getSize(); + var newDate = getLastModified(); + // The size check is intended for cases in which editors first clear a file prior to writing it + // In that case, multiple watch events are sent. If these happened very fast, it might be possible that + // the modified time is the same for both write operations due to the file system modified time resolution + // being limited + // We then can't identify changes purely based on the modified time, so the file size is the next best + // option + // This might result in double change detection in rare cases, but that is irrelevant as it prevents files + // from being blanked + var changed = !newDate.equals(lastModified) || newSize > lastSize; + lastSize = newSize; + lastModified = newDate; + return changed; } } } diff --git a/app/src/main/java/io/xpipe/app/util/FixedHierarchyStore.java b/app/src/main/java/io/xpipe/app/util/FixedHierarchyStore.java index 9191e3149..d06ad3e5f 100644 --- a/app/src/main/java/io/xpipe/app/util/FixedHierarchyStore.java +++ b/app/src/main/java/io/xpipe/app/util/FixedHierarchyStore.java @@ -9,5 +9,9 @@ import java.util.List; public interface FixedHierarchyStore extends DataStore { + default boolean removeLeftovers() { + return true; + } + List> listChildren(DataStoreEntry self) throws Exception; } diff --git a/app/src/main/java/io/xpipe/app/util/InputHelper.java b/app/src/main/java/io/xpipe/app/util/InputHelper.java index af2fecb18..e4c5a96a3 100644 --- a/app/src/main/java/io/xpipe/app/util/InputHelper.java +++ b/app/src/main/java/io/xpipe/app/util/InputHelper.java @@ -2,10 +2,7 @@ package io.xpipe.app.util; import javafx.event.EventHandler; import javafx.event.EventTarget; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyCombination; -import javafx.scene.input.KeyEvent; -import javafx.scene.input.MouseEvent; +import javafx.scene.input.*; import java.util.List; import java.util.function.Consumer; @@ -27,11 +24,7 @@ public class InputHelper { public static void onExactKeyCode(EventTarget target, KeyCode code, boolean filter, Consumer r) { EventHandler keyEventEventHandler = event -> { - if (event.isAltDown() || event.isShiftDown() || event.isShortcutDown()) { - return; - } - - if (code == event.getCode()) { + if (new KeyCodeCombination(code).match(event)) { r.accept(event); } }; @@ -42,20 +35,9 @@ public class InputHelper { } } - public static void onInput(EventTarget target, boolean filter, Consumer r) { - EventHandler keyEventEventHandler = event -> { - r.accept(event); - }; - if (filter) { - target.addEventFilter(KeyEvent.KEY_PRESSED, keyEventEventHandler); - } else { - target.addEventHandler(KeyEvent.KEY_PRESSED, keyEventEventHandler); - } - } - public static void onLeft(EventTarget target, boolean filter, Consumer r) { EventHandler e = event -> { - if (event.getCode() == KeyCode.LEFT || event.getCode() == KeyCode.NUMPAD4) { + if (new KeyCodeCombination(KeyCode.LEFT).match(event) || new KeyCodeCombination(KeyCode.NUMPAD4).match(event)) { r.accept(event); } }; @@ -68,7 +50,7 @@ public class InputHelper { public static void onRight(EventTarget target, boolean filter, Consumer r) { EventHandler e = event -> { - if (event.getCode() == KeyCode.RIGHT || event.getCode() == KeyCode.NUMPAD6) { + if (new KeyCodeCombination(KeyCode.RIGHT).match(event) || new KeyCodeCombination(KeyCode.NUMPAD6).match(event)) { r.accept(event); } }; diff --git a/app/src/main/java/io/xpipe/app/util/NativeBridge.java b/app/src/main/java/io/xpipe/app/util/NativeBridge.java new file mode 100644 index 000000000..b28039507 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/NativeBridge.java @@ -0,0 +1,44 @@ +package io.xpipe.app.util; + +import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.core.util.XPipeInstallation; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.NativeLong; + +import java.util.Map; +import java.util.Optional; + +public class NativeBridge { + + private static MacOsLibrary macOsLibrary; + private static boolean loadingFailed; + + public static Optional getMacOsLibrary() { + if (macOsLibrary == null && !loadingFailed) { + try { + System.setProperty( + "jna.library.path", + XPipeInstallation.getCurrentInstallationBasePath() + .resolve("Contents") + .resolve("runtime") + .resolve("Contents") + .resolve("Home") + .resolve("lib") + .toString()); + var l = Native.load("xpipe_bridge", MacOsLibrary.class, Map.of()); + macOsLibrary = l; + } catch (Throwable t) { + ErrorEvent.fromThrowable(t).handle(); + loadingFailed = true; + } + } + return Optional.ofNullable(macOsLibrary); + } + + public static interface MacOsLibrary extends Library { + + public abstract void setAppearance(NativeLong window, boolean seamlessFrame, boolean dark); + } +} 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 559140ba6..41f1205e3 100644 --- a/app/src/main/java/io/xpipe/app/util/PlatformState.java +++ b/app/src/main/java/io/xpipe/app/util/PlatformState.java @@ -6,7 +6,9 @@ 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 javafx.application.Platform; + import lombok.Getter; import lombok.Setter; import org.apache.commons.lang3.SystemUtils; @@ -121,7 +123,8 @@ public enum PlatformState { } if (SystemUtils.IS_OS_WINDOWS && ModifiedStage.mergeFrame()) { - // This is primarily intended to fix Windows unified stage transparency issues (https://bugs.openjdk.org/browse/JDK-8329382) + // This is primarily intended to fix Windows unified stage transparency issues + // (https://bugs.openjdk.org/browse/JDK-8329382) System.setProperty("prism.forceUploadingPainter", "true"); } @@ -152,7 +155,7 @@ public enum PlatformState { // Platform initialization has failed in this case PlatformState.setCurrent(PlatformState.EXITED); TrackEvent.error(t.getMessage()); - lastError =t; + lastError = t; return; } } 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 ef148f383..c2d8a7f80 100644 --- a/app/src/main/java/io/xpipe/app/util/ScanAlert.java +++ b/app/src/main/java/io/xpipe/app/util/ScanAlert.java @@ -40,7 +40,7 @@ public class ScanAlert { }); } - private static void showForShellStore(DataStoreEntry initial) { + public static void showForShellStore(DataStoreEntry initial) { show(initial, (DataStoreEntry entry, ShellControl sc) -> { if (!sc.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) { return null; @@ -141,6 +141,11 @@ public class ScanAlert { }); } + @Override + protected Comp pane(Comp content) { + return content; + } + @Override public Comp content() { StackPane stackPane = new StackPane(); @@ -166,7 +171,7 @@ public class ScanAlert { .apply(struc -> { VBox.setVgrow(struc.get().getChildren().get(1), ALWAYS); }) - .padding(new Insets(20)); + .padding(new Insets(5, 20, 20, 20)); entry.subscribe(newValue -> { selected.clear(); 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 44882af95..3645b97e1 100644 --- a/app/src/main/java/io/xpipe/app/util/SecretRetrievalStrategy.java +++ b/app/src/main/java/io/xpipe/app/util/SecretRetrievalStrategy.java @@ -180,6 +180,10 @@ public interface SecretRetrievalStrategy { return new SecretQuery() { @Override public SecretQueryResult query(String prompt) { + if (command == null || command.isBlank()) { + throw ErrorEvent.expected(new IllegalStateException("No custom command specified")); + } + try (var cc = new LocalStore().control().command(command).start()) { return new SecretQueryResult( InPlaceSecretValue.of(cc.readStdoutOrThrow()), SecretQueryState.NORMAL); diff --git a/app/src/main/java/io/xpipe/app/util/ShellTemp.java b/app/src/main/java/io/xpipe/app/util/ShellTemp.java index 952de29b5..9b8173fdd 100644 --- a/app/src/main/java/io/xpipe/app/util/ShellTemp.java +++ b/app/src/main/java/io/xpipe/app/util/ShellTemp.java @@ -26,6 +26,7 @@ public class ShellTemp { temp = temp.resolve(user != null ? user : "user"); try { + FileUtils.forceMkdir(temp.toFile()); // We did not set this in earlier versions. If we are running as a different user, it might fail Files.setPosixFilePermissions(temp, PosixFilePermissions.fromString("rwxrwxrwx")); } catch (Exception e) { diff --git a/app/src/main/java/io/xpipe/app/util/StringSource.java b/app/src/main/java/io/xpipe/app/util/StringSource.java deleted file mode 100644 index b92a639be..000000000 --- a/app/src/main/java/io/xpipe/app/util/StringSource.java +++ /dev/null @@ -1,51 +0,0 @@ -package io.xpipe.app.util; - -import io.xpipe.app.issue.ErrorEvent; -import io.xpipe.app.storage.ContextualFileReference; -import io.xpipe.core.store.ShellStore; - -import lombok.EqualsAndHashCode; -import lombok.Value; - -public abstract class StringSource { - - public abstract String get() throws Exception; - - @Value - @EqualsAndHashCode(callSuper = true) - public static class InPlace extends StringSource { - - String value; - - @Override - public String get() { - return value; - } - } - - @Value - @EqualsAndHashCode(callSuper = true) - public static class File extends StringSource { - - ShellStore host; - ContextualFileReference file; - - @Override - public String get() throws Exception { - if (host == null || file == null) { - return ""; - } - - try (var sc = host.control().start()) { - var path = file.toAbsoluteFilePath(sc); - if (!sc.getShellDialect().createFileExistsCommand(sc, path).executeAndCheck()) { - throw ErrorEvent.expected(new IllegalArgumentException("File " + path + " does not exist")); - } - - var abs = file.toAbsoluteFilePath(sc); - var content = sc.getShellDialect().getFileReadCommand(sc, abs).readStdoutOrThrow(); - return content; - } - } - } -} 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 e13427780..3f69f5279 100644 --- a/app/src/main/java/io/xpipe/app/util/UnlockAlert.java +++ b/app/src/main/java/io/xpipe/app/util/UnlockAlert.java @@ -1,20 +1,8 @@ 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.window.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.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 { @@ -28,40 +16,9 @@ public class UnlockAlert { return; } - PlatformState.initPlatformOrThrow(); - AppI18n.init(); - AppStyle.init(); - AppTheme.init(); - while (true) { - 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 text = new SecretFieldComp(pw, false).createRegion(); - text.setStyle("-fx-border-width: 1px"); - - 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()) { + var r = AskpassAlert.queryRaw(AppI18n.get("unlockAlertHeader"), null); + if (r.getState() == SecretQueryState.CANCELLED) { ErrorEvent.fromMessage("Unlock cancelled") .expected() .term() @@ -70,7 +27,7 @@ public class UnlockAlert { return; } - if (AppPrefs.get().unlock(pw.get())) { + if (AppPrefs.get().unlock(r.getSecret().inPlace())) { 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 ce952d374..a2aca1d11 100644 --- a/app/src/main/java/io/xpipe/app/util/WindowsRegistry.java +++ b/app/src/main/java/io/xpipe/app/util/WindowsRegistry.java @@ -145,23 +145,20 @@ public abstract class WindowsRegistry { .add("/v") .addQuoted(valueName); - String output; - try (var c = shellControl.command(command).start()) { - output = c.readStdoutDiscardErr(); - if (c.getExitCode() != 0) { - return Optional.empty(); - } + var output = shellControl.command(command).readStdoutIfPossible(); + if (output.isEmpty()) { + return Optional.empty(); } // Output has the following format: // \n\n\n\t\t - if (output.contains("\t")) { - String[] parsed = output.split("\t"); + if (output.get().contains("\t")) { + String[] parsed = output.get().split("\t"); return Optional.of(parsed[parsed.length - 1]); } - if (output.contains(" ")) { - String[] parsed = output.split(" "); + if (output.get().contains(" ")) { + String[] parsed = output.get().split(" "); return Optional.of(parsed[parsed.length - 1]); } @@ -176,14 +173,7 @@ public abstract class WindowsRegistry { .add("/v") .addQuoted(valueName) .add("/s"); - try (var c = shellControl.command(command).start()) { - var output = c.readStdoutDiscardErr(); - if (c.getExitCode() != 0) { - return Optional.empty(); - } else { - return Optional.of(output); - } - } + return shellControl.command(command).readStdoutIfPossible(); } @Override @@ -196,22 +186,17 @@ public abstract class WindowsRegistry { .add("/s") .add("/e") .add("/d"); - try (var c = shellControl.command(command).start()) { - var output = c.readStdoutDiscardErr(); - if (c.getExitCode() != 0) { + return shellControl.command(command).readStdoutIfPossible().flatMap(output -> { + return output.lines().findFirst().flatMap(s -> { + if (s.startsWith("HKEY_CURRENT_USER\\")) { + return Optional.of(new Key(HKEY_CURRENT_USER, s.replace("HKEY_CURRENT_USER\\", ""))); + } + if (s.startsWith("HKEY_LOCAL_MACHINE\\")) { + return Optional.of(new Key(HKEY_LOCAL_MACHINE, s.replace("HKEY_LOCAL_MACHINE\\", ""))); + } return Optional.empty(); - } else { - return output.lines().findFirst().flatMap(s -> { - if (s.startsWith("HKEY_CURRENT_USER\\")) { - return Optional.of(new Key(HKEY_CURRENT_USER, s.replace("HKEY_CURRENT_USER\\", ""))); - } - if (s.startsWith("HKEY_LOCAL_MACHINE\\")) { - return Optional.of(new Key(HKEY_LOCAL_MACHINE, s.replace("HKEY_LOCAL_MACHINE\\", ""))); - } - return Optional.empty(); - }); - } - } + }); + }); } } } diff --git a/app/src/main/resources/io/xpipe/app/resources/misc/api.md b/app/src/main/resources/io/xpipe/app/resources/misc/api.md index 032417a5d..0fb4a6f37 100644 --- a/app/src/main/resources/io/xpipe/app/resources/misc/api.md +++ b/app/src/main/resources/io/xpipe/app/resources/misc/api.md @@ -1673,6 +1673,7 @@ These errors will be returned with the HTTP return code 500. "shellDialect": 0, "osType": "string", "osName": "string", + "ttyState": "string", "temp": "string" } ``` @@ -2969,6 +2970,7 @@ undefined "shellDialect": 0, "osType": "string", "osName": "string", + "ttyState": "string", "temp": "string" } @@ -2981,6 +2983,7 @@ undefined |shellDialect|integer|true|none|The shell dialect| |osType|string|true|none|The general type of operating system| |osName|string|true|none|The display name of the operating system| +|ttyState|string|false|none|Whether a tty/pty has been allocated for the connection. If allocated, input and output will be unreliable. It is not recommended to use a shell connection then.| |temp|string|true|none|The location of the temporary directory|

ShellStopRequest

@@ -3535,7 +3538,7 @@ xor |Name|Type|Required|Restrictions|Description| |---|---|---|---|---| -|*anonymous*|[Local](#schemalocal)|false|none|Authentication method for local applications. Uses file system access as proof of authentication.| +|*anonymous*|[Local](#schemalocal)|false|none|Authentication method for local applications. Uses file system access as proof of authentication.

You can find the authentication file at:
- %TEMP%\xpipe_auth on Windows
- $TMP/xpipe_auth on Linux
- $TMPDIR/xpipe_auth on macOS

For the PTB releases the file name is changed to xpipe_ptb_auth to prevent collisions.

As the temporary directory on Linux is global, the daemon might run as another user and your current user might not have permissions to access the auth file.|

ApiKey

@@ -3578,12 +3581,21 @@ API key authentication Authentication method for local applications. Uses file system access as proof of authentication. +You can find the authentication file at: +- %TEMP%\xpipe_auth on Windows +- $TMP/xpipe_auth on Linux +- $TMPDIR/xpipe_auth on macOS + +For the PTB releases the file name is changed to xpipe_ptb_auth to prevent collisions. + +As the temporary directory on Linux is global, the daemon might run as another user and your current user might not have permissions to access the auth file. +

Properties

|Name|Type|Required|Restrictions|Description| |---|---|---|---|---| |type|string|true|none|none| -|authFileContent|string|true|none|The contents of the local file $TEMP/xpipe_auth. This file is automatically generated when XPipe starts.| +|authFileContent|string|true|none|The contents of the local file /xpipe_auth. This file is automatically generated when XPipe starts.|

ClientInformation

diff --git a/app/src/main/resources/io/xpipe/app/resources/misc/welcome.md b/app/src/main/resources/io/xpipe/app/resources/misc/welcome.md index 3d99b6073..9b4c4dc32 100644 --- a/app/src/main/resources/io/xpipe/app/resources/misc/welcome.md +++ b/app/src/main/resources/io/xpipe/app/resources/misc/welcome.md @@ -1,11 +1,10 @@ ## Welcome -Thank you for using XPipe! +Welcome to XPipe! + You can view the development status, report issues, and more at the following places: - [GitHub Repository](https://github.com/xpipe-io/xpipe/) - [Discord Server](https://discord.gg/8y89vS8cRb) - [Slack Server](https://join.slack.com/t/XPipe/shared_invite/zt-1awjq0t5j-5i4UjNJfNe1VN4b_auu6Cg) -- [Email me](mailto://crschnick@xpipe.io) - -Note that the XPipe project currently is a one-man show, but I still try to respond to everything in time. +- [Email us](mailto://hello@xpipe.io) 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 068cb1a08..94f1783df 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 @@ -33,8 +33,8 @@ } .bookmarks-header { - -fx-min-height: 3.5em; - -fx-pref-height: 3.5em; - -fx-max-height: 3.5em; + -fx-min-height: 3.3em; + -fx-pref-height: 3.3em; + -fx-max-height: 3.3em; -fx-padding: 9 6 9 8; } 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 6fd8e80b1..2d7e6ee79 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 @@ -15,7 +15,7 @@ -fx-padding: 0 6 8 8; } -.transfer > * { +.transfer > .download-background { -fx-border-radius: 4; -fx-background-radius: 4; -fx-border-color: -color-border-default; @@ -23,7 +23,7 @@ -fx-background-color: -color-bg-subtle; } -.transfer:highlighted > * { +.transfer:highlighted > .download-background { -fx-border-color: -color-accent-emphasis; -fx-background-color: derive(-color-bg-subtle, 5%); } @@ -43,7 +43,7 @@ } .transfer .button:hover { - -fx-background-color: -color-bg-subtle; + -fx-background-color: -color-accent-subtle; -fx-opacity: 1.0; } @@ -56,7 +56,7 @@ } .root:seamless-frame .browser .top-spacer { - -fx-background-radius: 0 10 0 0; + -fx-background-radius: 0 6 0 0; } .browser .welcome .button:hover { @@ -95,9 +95,9 @@ } .browser .top-bar { - -fx-min-height: 3.5em; - -fx-pref-height: 3.5em; - -fx-max-height: 3.5em; + -fx-min-height: 3.3em; + -fx-pref-height: 3.3em; + -fx-max-height: 3.3em; -fx-padding: 9px 6px; } @@ -145,7 +145,7 @@ } .browser .breadcrumbs { - -fx-padding: 2px 10px 2px 10px; + -fx-padding: 0px 10px 0px 10px; } .browser .breadcrumbs { @@ -162,7 +162,7 @@ } .browser .path-text, .browser .browser-filter .text-field { - -fx-padding: 6 12; + -fx-padding: 3 12; } .browser .path-text:invisible { @@ -203,7 +203,22 @@ -fx-max-height: 2.65em; } -.browser .tab-header-area { + +.browser .tab-header-area .control-buttons-tab { + -fx-opacity: 0; +} + +.browser .tab-loading-indicator .loading-comp { + -fx-min-width: 2.5em; + -fx-pref-width: 2.5em; + -fx-max-width: 2.5em; + -fx-min-height: 2.5em; + -fx-pref-height: 2.5em; + -fx-max-height: 2.5em; + -fx-background-color: transparent; +} + +.browser .tab-pane.floating > .tab-header-area { -fx-border-width: 0 0 0.05em 0; -fx-border-color: -color-border-default; } @@ -223,13 +238,13 @@ } .root:seamless-frame .browser .tab-header-area { - -fx-background-radius: 0 10 0 0; + -fx-background-radius: 0 6 0 0; } .browser .browser-content { -fx-padding: 6 0 0 0; - -fx-border-radius: 10 10 4 4; - -fx-background-radius: 10 10 4 4; + -fx-border-radius: 4; + -fx-background-radius: 4; -fx-background-color: -color-bg-subtle, -color-bg-default; -fx-background-insets: 0, 7 0 0 0; -fx-border-width: 1; diff --git a/app/src/main/resources/io/xpipe/app/resources/style/choice-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/choice-comp.css index 2127a3e4f..b138acd5f 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/choice-comp.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/choice-comp.css @@ -3,7 +3,7 @@ } .choice-comp-content > .top { - -fx-padding: 0.5em 1em 0.5em 1em; + -fx-padding: 0.4em; -fx-background-color: -color-neutral-subtle; -fx-border-width: 1 0 1 0; -fx-border-color: -color-border-default; diff --git a/app/src/main/resources/io/xpipe/app/resources/style/filter-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/filter-comp.css deleted file mode 100644 index 331fd8c3c..000000000 --- a/app/src/main/resources/io/xpipe/app/resources/style/filter-comp.css +++ /dev/null @@ -1,4 +0,0 @@ -.filter-comp { - -fx-padding: 0.15em 0.3em 0.15em 0.3em; - -fx-background-color: transparent; -} diff --git a/app/src/main/resources/io/xpipe/app/resources/style/named-choice-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/named-choice-comp.css deleted file mode 100644 index bf1937c57..000000000 --- a/app/src/main/resources/io/xpipe/app/resources/style/named-choice-comp.css +++ /dev/null @@ -1,8 +0,0 @@ -.named-store-choice, .named-source-choice { - -fx-border-width: 1px; - -fx-border-color: -color-accent-fg; - -fx-background-color: transparent; - -fx-border-radius: 4px; - -fx-padding: 2px; - -fx-background-radius: 4px; -} \ No newline at end of file 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 d6ceeff26..f00863e4c 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 @@ -35,31 +35,26 @@ -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; + -fx-padding: 0.2em 0 0 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; + -fx-padding: 0.6em 1em 0.6em 1em; + -fx-background-radius: 0, 4, 4; + -fx-background-insets: 0, 2 4 2 4, 3 5 3 5; } .prefs .sidebar .button:selected { - -fx-background-color: -color-accent-subtle; - -fx-border-color: -color-accent-emphasis; - -fx-border-width: 1 0 1 0; + -fx-background-color: transparent, -color-border-default, -color-bg-default; } .prefs .sidebar .button:armed { - -fx-background-color: derive(-color-neutral-muted, 25%); + -fx-background-color: transparent, -color-accent-muted, derive(-color-neutral-muted, 25%); } .prefs .sidebar .button:hover, .root:key-navigation .prefs .sidebar .button:focused { - -fx-background-color: -color-neutral-muted; + -fx-background-color: transparent, -color-border-default, -color-bg-overlay; } .prefs .theme-switcher .combo-box-popup .list-view { 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 9bc8f7283..df2abab39 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 @@ -18,4 +18,4 @@ .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 317444173..855e4f9bc 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 @@ -30,7 +30,7 @@ } .sidebar-comp .icon-button-comp { - -fx-padding: 1em; + -fx-padding: 1.1em; } .sidebar-comp .icon-button-comp .vbox { 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 10a77eb29..5710d3738 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 @@ -25,14 +25,6 @@ -fx-text-fill: #ee4829; } -.store-entry-grid:incomplete .summary { - -fx-text-fill: #ee4829; -} - -.store-entry-grid:incomplete .information { - -fx-text-fill: #ee4829; -} - .store-entry-grid:incomplete .icon { -fx-opacity: 0.5; } @@ -90,6 +82,14 @@ -fx-opacity: 0.2; } +.store-entry-comp .button-bar { + -fx-padding: 5; +} + +.store-entry-grid.dense .button-bar { + -fx-padding: 3; +} + .store-entry-comp .button-bar .button { -fx-padding: 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 a19000a00..b9c77be98 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 @@ -9,6 +9,10 @@ -fx-background-color: transparent; } +.root:macos:seamless-frame { + -fx-padding: 0 0 27 0; +} + .root:dark:separate-frame .background { -fx-background-color: derive(-color-bg-default, 1%); } @@ -46,16 +50,21 @@ .root:seamless-frame.layout > .background { -fx-background-insets: 5 0 0 0; -fx-border-insets: 5 0 0 0; - -fx-background-radius: 0 10 0 0; - -fx-border-radius: 0 10 0 0; + -fx-background-radius: 0 6 0 0; + -fx-border-radius: 0 6 0 0; -fx-border-width: 1 1 0 0; -fx-border-color: -color-border-default; -fx-padding: 0 0 0 0; } +.root:macos:seamless-frame.layout > .background { + -fx-background-insets: 0; + -fx-border-insets: 0; +} + .root:seamless-frame.layout > .background > * { - -fx-background-radius: 0 10 0 0; - -fx-border-radius: 0 10 0 0; + -fx-background-radius: 0 6 0 0; + -fx-border-radius: 0 6 0 0; } .toggle-switch:has-graphic .label { @@ -63,7 +72,7 @@ } .toggle-switch:has-graphic { - -fx-font-size: 0.8em; + -fx-font-size: 0.75em; } .store-layout .split-pane-divider { @@ -87,12 +96,16 @@ -fx-background-radius: 4px; -fx-border-width: 0.05em; -fx-border-radius: 4px; - -fx-padding: 1em; + -fx-padding: 1px; -fx-background-color: -color-bg-default; -fx-border-color: -color-neutral-emphasis; } -.text { +.scan-list .list-content { + -fx-padding: 0.7em 1px 1em 1em; +} + +* { -fx-font-smoothing-type: gray; } diff --git a/beacon/build.gradle b/beacon/build.gradle index 096dd649b..e6a912921 100644 --- a/beacon/build.gradle +++ b/beacon/build.gradle @@ -17,8 +17,8 @@ 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' + compileOnly 'org.junit.jupiter:junit-jupiter-api:5.10.3' + compileOnly 'org.junit.jupiter:junit-jupiter-params:5.10.3' 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 be1e2dae7..16dbbec7e 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java @@ -24,6 +24,15 @@ public class BeaconClient { this.port = port; } + public static boolean isOccupied(int port) { + var file = XPipeInstallation.getLocalBeaconAuthFile(); + var reachable = BeaconServer.isReachable(port); + if (!Files.exists(file) && !reachable) { + return false; + } + return reachable; + } + public static BeaconClient establishConnection(int port, BeaconClientInformation information) throws Exception { var client = new BeaconClient(port); var auth = Files.readString(XPipeInstallation.getLocalBeaconAuthFile()); @@ -55,7 +64,8 @@ public class BeaconClient { var client = HttpClient.newHttpClient(); HttpResponse response; try { - var uri = URI.create("http://localhost:" + port + prov.getPath()); + // Use direct IP to prevent DNS lookups and potential blocks (e.g. portmaster) + var uri = URI.create("http://127.0.0.1:" + port + prov.getPath()); var builder = HttpRequest.newBuilder(); if (token != null) { builder.header("Authorization", "Bearer " + token); diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconInterface.java b/beacon/src/main/java/io/xpipe/beacon/BeaconInterface.java index 0e28a2f03..91742971c 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconInterface.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconInterface.java @@ -62,6 +62,10 @@ public abstract class BeaconInterface { return (Class) Class.forName(name); } + public boolean acceptInShutdown() { + return false; + } + public boolean requiresCompletedStartup() { return true; } diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java b/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java index 3de5f910b..838058c45 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java @@ -8,7 +8,7 @@ import io.xpipe.core.util.XPipeInstallation; import java.io.BufferedReader; import java.io.InputStreamReader; -import java.net.InetAddress; +import java.net.Inet4Address; import java.net.InetSocketAddress; import java.net.Socket; import java.util.List; @@ -20,7 +20,7 @@ public class BeaconServer { public static boolean isReachable(int port) { try (var socket = new Socket()) { - socket.connect(new InetSocketAddress(InetAddress.getLoopbackAddress(), port), 5000); + socket.connect(new InetSocketAddress(Inet4Address.getByAddress(new byte[]{ 0x7f,0x00,0x00,0x01 }), port), 5000); return true; } catch (Exception e) { return false; diff --git a/beacon/src/main/java/io/xpipe/beacon/api/AskpassExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/AskpassExchange.java index a2674cc76..eb7587ef1 100644 --- a/beacon/src/main/java/io/xpipe/beacon/api/AskpassExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/api/AskpassExchange.java @@ -12,6 +12,11 @@ import java.util.UUID; public class AskpassExchange extends BeaconInterface { + @Override + public boolean acceptInShutdown() { + return true; + } + @Override public String getPath() { return "/askpass"; diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionAddExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionAddExchange.java index 64ab54cd8..fec2a8841 100644 --- a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionAddExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionAddExchange.java @@ -2,6 +2,7 @@ package io.xpipe.beacon.api; import io.xpipe.beacon.BeaconInterface; import io.xpipe.core.store.DataStore; + import lombok.Builder; import lombok.NonNull; import lombok.Value; diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionBrowseExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionBrowseExchange.java index d5c1dd249..f10b9dc57 100644 --- a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionBrowseExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionBrowseExchange.java @@ -1,6 +1,7 @@ package io.xpipe.beacon.api; import io.xpipe.beacon.BeaconInterface; + import lombok.Builder; import lombok.NonNull; import lombok.Value; diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionRefreshExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionRefreshExchange.java index aea789314..2646f0e6e 100644 --- a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionRefreshExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionRefreshExchange.java @@ -1,6 +1,7 @@ package io.xpipe.beacon.api; import io.xpipe.beacon.BeaconInterface; + import lombok.Builder; import lombok.NonNull; import lombok.Value; diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionRemoveExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionRemoveExchange.java index 600f443d8..530f7b0cd 100644 --- a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionRemoveExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionRemoveExchange.java @@ -1,6 +1,7 @@ package io.xpipe.beacon.api; import io.xpipe.beacon.BeaconInterface; + import lombok.Builder; import lombok.NonNull; import lombok.Value; diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionTerminalExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionTerminalExchange.java index 8b74204e8..0fbd1138f 100644 --- a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionTerminalExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionTerminalExchange.java @@ -1,6 +1,7 @@ package io.xpipe.beacon.api; import io.xpipe.beacon.BeaconInterface; + import lombok.Builder; import lombok.NonNull; import lombok.Value; diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionToggleExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionToggleExchange.java index 5a1bc1829..dcce1da17 100644 --- a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionToggleExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionToggleExchange.java @@ -1,6 +1,7 @@ package io.xpipe.beacon.api; import io.xpipe.beacon.BeaconInterface; + import lombok.Builder; import lombok.NonNull; import lombok.Value; diff --git a/beacon/src/main/java/io/xpipe/beacon/api/DaemonVersionExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/DaemonVersionExchange.java index cd1410acd..aa40a5f2e 100644 --- a/beacon/src/main/java/io/xpipe/beacon/api/DaemonVersionExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/api/DaemonVersionExchange.java @@ -26,12 +26,16 @@ public class DaemonVersionExchange extends BeaconInterface { + @Override + public boolean acceptInShutdown() { + return true; + } + @Override public String getPath() { return "/handshake"; diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ShellStartExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/ShellStartExchange.java index 10f32b12b..4d4115e29 100644 --- a/beacon/src/main/java/io/xpipe/beacon/api/ShellStartExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/api/ShellStartExchange.java @@ -3,6 +3,7 @@ package io.xpipe.beacon.api; import io.xpipe.beacon.BeaconInterface; import io.xpipe.core.process.OsType; import io.xpipe.core.process.ShellDialect; +import io.xpipe.core.process.ShellTtyState; import io.xpipe.core.store.FilePath; import lombok.Builder; @@ -40,6 +41,9 @@ public class ShellStartExchange extends BeaconInterface } } + +def user = project.hasProperty('sonatypeUsername') ? project.property('sonatypeUsername') : System.getenv('SONATYPE_USERNAME') +def pass = project.hasProperty('sonatypePassword') ? project.property('sonatypePassword') : System.getenv('SONATYPE_PASSWORD') + +tasks.withType(GenerateModuleMetadata) { + enabled = false +} + +nexusPublishing { + repositories { + sonatype { + nexusUrl.set(uri('https://s01.oss.sonatype.org/service/local/')) + snapshotRepositoryUrl.set(uri('https://s01.oss.sonatype.org/content/repositories/snapshots/')) + username = user + password = pass + } + } + useStaging = true +} + var devProps = file("$rootDir/app/dev.properties") if (!devProps.exists()) { devProps.text = file("$rootDir/gradle/gradle_scripts/dev_default.properties").text @@ -65,13 +85,13 @@ def getArchName() { def getPlatformName() { def currentOS = DefaultNativePlatform.currentOperatingSystem; - def platform = null + def platform if (currentOS.isWindows()) { platform = 'windows' - } else if (currentOS.isLinux()) { - platform = 'linux' - } else if (currentOS.isMacOsX()) { + } else if (currentOS.isMacOsX()) { platform = 'osx' + } else { + platform = 'linux' } return platform; } @@ -122,11 +142,13 @@ project.ext { "-Dio.xpipe.app.arch=$rootProject.arch", "-Dio.xpipe.app.languages=${String.join(",", languages)}", "-Dfile.encoding=UTF-8", - // Disable this for now as it requires Windows 10+ - // '-XX:+UseZGC', "-Dvisualvm.display.name=XPipe", "-Djavafx.preloader=io.xpipe.app.core.AppPreloader" ] + // Disable this on Windows for now as it requires Windows 10+ + if (org.gradle.internal.os.OperatingSystem.current().isLinux() || org.gradle.internal.os.OperatingSystem.current().isMacOsX()) { + jvmRunArgs += ['-XX:+UseZGC'] + } if (org.gradle.internal.os.OperatingSystem.current().isMacOsX()) { jvmRunArgs += ["-Dapple.awt.application.appearance=system"] } @@ -212,3 +234,6 @@ task testAll(type: DefaultTask) { } finalizedBy(testReport) } + +group = 'io.xpipe' +version = versionString \ No newline at end of file diff --git a/core/build.gradle b/core/build.gradle index df8952576..c8478cb37 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -13,8 +13,8 @@ compileJava { } dependencies { - api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.17.1" - implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.17.1" + api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.17.2" + implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.17.2" } version = rootProject.versionString 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 3e2904845..fa9b90a41 100644 --- a/core/src/main/java/io/xpipe/core/process/CommandBuilder.java +++ b/core/src/main/java/io/xpipe/core/process/CommandBuilder.java @@ -255,6 +255,10 @@ public class CommandBuilder { } public String buildFull(ShellControl sc) throws Exception { + if (sc == null) { + return buildSimple(); + } + var s = buildBase(sc); LinkedHashMap map = new LinkedHashMap<>(); for (var e : environmentVariables.entrySet()) { 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 7849cf087..2e76586e0 100644 --- a/core/src/main/java/io/xpipe/core/process/CommandControl.java +++ b/core/src/main/java/io/xpipe/core/process/CommandControl.java @@ -1,15 +1,10 @@ package io.xpipe.core.process; -import io.xpipe.core.util.FailableConsumer; - -import com.fasterxml.jackson.databind.JsonNode; - import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; import java.nio.charset.Charset; +import java.time.Duration; import java.util.Optional; -import java.util.function.Consumer; import java.util.function.Function; public interface CommandControl extends ProcessControl { @@ -59,6 +54,8 @@ public interface CommandControl extends ProcessControl { OutputStream startExternalStdin() throws Exception; + public void setExitTimeout(Duration duration); + boolean waitFor(); CommandControl withCustomCharset(Charset charset); @@ -67,32 +64,14 @@ public interface CommandControl extends ProcessControl { CommandControl elevated(ElevationFunction function); - void withStdoutOrThrow(FailableConsumer c); - String[] readStdoutAndStderr() throws Exception; - String readStdoutDiscardErr() throws Exception; - - String readStderrDiscardStdout() throws Exception; - void discardOrThrow() throws Exception; - void accumulateStdout(Consumer con); - - void accumulateStderr(Consumer con); - byte[] readRawBytesOrThrow() throws Exception; String readStdoutOrThrow() throws Exception; - JsonNode readStdoutJsonOrThrow() throws Exception; - - String readStderrOrThrow() throws Exception; - - String readStdoutAndWait() throws Exception; - - String readStderrAndWait() throws Exception; - Optional readStdoutIfPossible() throws Exception; default boolean discardAndCheckExit() throws ProcessOutputException { @@ -110,10 +89,6 @@ 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/CommandFeedbackPredicate.java b/core/src/main/java/io/xpipe/core/process/CommandFeedbackPredicate.java deleted file mode 100644 index 24d569580..000000000 --- a/core/src/main/java/io/xpipe/core/process/CommandFeedbackPredicate.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.xpipe.core.process; - -public interface CommandFeedbackPredicate { - - boolean test(CommandBuilder command); -} 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 9882e409e..8d454b044 100644 --- a/core/src/main/java/io/xpipe/core/process/OsType.java +++ b/core/src/main/java/io/xpipe/core/process/OsType.java @@ -4,8 +4,6 @@ import io.xpipe.core.store.FileNames; import java.util.List; import java.util.Locale; -import java.util.Map; -import java.util.stream.Collectors; public interface OsType { @@ -21,10 +19,8 @@ public interface OsType { return MACOS; } else if (osName.contains("win")) { return WINDOWS; - } else if (osName.contains("nux")) { - return LINUX; } else { - throw new UnsupportedOperationException("Unknown operating system"); + return LINUX; } } @@ -38,14 +34,10 @@ public interface OsType { String getName(); - String getTempDirectory(ShellControl pc) throws Exception; - - Map getProperties(ShellControl pc) throws Exception; - - String determineOperatingSystemName(ShellControl pc) throws Exception; - sealed interface Local extends OsType permits OsType.Windows, OsType.Linux, OsType.MacOs { + String getId(); + default Any toAny() { return (Any) this; } @@ -88,48 +80,8 @@ public interface OsType { } @Override - public String getTempDirectory(ShellControl pc) throws Exception { - var def = pc.executeSimpleStringCommand(pc.getShellDialect().getPrintEnvironmentVariableCommand("TEMP")); - if (!def.isBlank() && pc.getShellDialect().directoryExists(pc, def).executeAndCheck()) { - return def; - } - - var fallback = pc.executeSimpleStringCommand( - pc.getShellDialect().getPrintEnvironmentVariableCommand("LOCALAPPDATA")); - if (!fallback.isBlank() - && pc.getShellDialect().directoryExists(pc, fallback).executeAndCheck()) { - return fallback; - } - - return def; - } - - @Override - public Map getProperties(ShellControl pc) throws Exception { - try (CommandControl c = pc.command("systeminfo").start()) { - var text = c.readStdoutOrThrow(); - return PropertiesFormatsParser.parse(text, ":"); - } - } - - @Override - public String determineOperatingSystemName(ShellControl pc) { - try { - return pc.executeSimpleStringCommand("wmic os get Caption") - .lines() - .skip(1) - .collect(Collectors.joining()) - .trim() - + " " - + pc.executeSimpleStringCommand("wmic os get Version") - .lines() - .skip(1) - .collect(Collectors.joining()) - .trim(); - } catch (Throwable t) { - // Just in case this fails somehow - return "Windows"; - } + public String getId() { + return "windows"; } } @@ -145,7 +97,7 @@ public interface OsType { public List determineInterestingPaths(ShellControl pc) throws Exception { var home = getHomeDirectory(pc); return List.of( - home, FileNames.join(home, "Downloads"), FileNames.join(home, "Documents"), "/etc", "/tmp", "/var"); + home, "/home", FileNames.join(home, "Downloads"), FileNames.join(home, "Documents"), "/etc", "/tmp", "/var"); } @Override @@ -163,58 +115,15 @@ public interface OsType { return "Linux"; } - @Override - public String getTempDirectory(ShellControl pc) { - return "/tmp/"; - } - - @Override - public Map getProperties(ShellControl pc) { - return null; - } - - @Override - public String determineOperatingSystemName(ShellControl pc) throws Exception { - String type = "Unknown"; - try (CommandControl c = pc.command("uname -o").start()) { - var text = c.readStdoutDiscardErr(); - if (c.getExitCode() == 0) { - type = text.strip(); - } - } - - String version = "?"; - try (CommandControl c = pc.command("uname -r").start()) { - var text = c.readStdoutDiscardErr(); - if (c.getExitCode() == 0) { - version = text.strip(); - } - } - - return type + " " + version; - } } 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); + public String getId() { + return "linux"; } + } final class Solaris extends Unix implements Any {} @@ -223,6 +132,11 @@ public interface OsType { final class MacOs implements OsType, Local, Any { + @Override + public String getId() { + return "macos"; + } + @Override public String makeFileSystemCompatible(String name) { // Technically the backslash is supported, but it causes all kinds of troubles, so we also exclude it @@ -258,34 +172,5 @@ public interface OsType { return "Mac"; } - @Override - public String getTempDirectory(ShellControl pc) throws Exception { - var found = pc.executeSimpleStringCommand(pc.getShellDialect().getPrintVariableCommand("TMPDIR")); - - // This variable is not defined for root users, so manually fix it. Why? ... - if (found.isBlank()) { - return "/tmp"; - } - - return found; - } - - @Override - public Map getProperties(ShellControl pc) throws Exception { - try (CommandControl c = pc.command("sw_vers").start()) { - var text = c.readStdoutOrThrow(); - return PropertiesFormatsParser.parse(text, ":"); - } - } - - @Override - public String determineOperatingSystemName(ShellControl pc) throws Exception { - var properties = getProperties(pc); - var name = pc.executeSimpleStringCommand( - "awk '/SOFTWARE LICENSE AGREEMENT FOR macOS/' '/System/Library/CoreServices/Setup " - + "Assistant.app/Contents/Resources/en.lproj/OSXSoftwareLicense.rtf' | " - + "awk -F 'macOS ' '{print $NF}' | awk '{print substr($0, 0, length($0)-1)}'"); - return properties.get("ProductName") + " " + name + " " + properties.get("ProductVersion"); - } } } 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 3c1efe058..f0cb5af7d 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellControl.java +++ b/core/src/main/java/io/xpipe/core/process/ShellControl.java @@ -18,6 +18,8 @@ import java.util.function.Function; public interface ShellControl extends ProcessControl { + ShellTtyState getTtyState(); + void setNonInteractive(); boolean isInteractive(); @@ -65,6 +67,7 @@ public interface ShellControl extends ProcessControl { var s = store.getState().toBuilder() .osType(shellControl.getOsType()) .shellDialect(shellControl.getOriginalShellDialect()) + .ttyState(shellControl.getTtyState()) .running(true) .osName(shellControl.getOsName()) .build(); @@ -113,25 +116,12 @@ public interface ShellControl extends ProcessControl { script)); } - default byte[] executeSimpleRawBytesCommand(String command) throws Exception { - try (CommandControl c = command(command).start()) { - return c.readRawBytesOrThrow(); - } - } - default String executeSimpleStringCommand(String command) throws Exception { try (CommandControl c = command(command).start()) { return c.readStdoutOrThrow(); } } - default Optional executeSimpleStringCommandAndCheck(String command) throws Exception { - try (CommandControl c = command(command).start()) { - var out = c.readStdoutDiscardErr(); - return c.getExitCode() == 0 ? Optional.of(out) : Optional.empty(); - } - } - default boolean executeSimpleBooleanCommand(String command) throws Exception { try (CommandControl c = command(command).start()) { return c.discardAndCheckExit(); @@ -150,20 +140,6 @@ public interface ShellControl extends ProcessControl { } } - default void executeSimpleCommand(String command, String failMessage) throws Exception { - try (CommandControl c = command(command).start()) { - c.discardOrThrow(); - } catch (ProcessOutputException out) { - throw ProcessOutputException.withPrefix(failMessage, out); - } - } - - default String executeSimpleStringCommand(ShellDialect type, String command) throws Exception { - try (var sub = subShell(type).start()) { - return sub.executeSimpleStringCommand(command); - } - } - ShellControl withSecurityPolicy(ShellSecurityPolicy policy); ShellSecurityPolicy getEffectiveSecurityPolicy(); @@ -232,10 +208,6 @@ public interface ShellControl extends ProcessControl { ShellControl singularSubShell(ShellOpenFunction command); - void writeLineAndReadEcho(String command) throws Exception; - - void writeLineAndReadEcho(String command, boolean log) throws Exception; - void cd(String directory) throws Exception; default CommandControl command(String command) { 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 231a7b30f..e4f474ef3 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellDialect.java +++ b/core/src/main/java/io/xpipe/core/process/ShellDialect.java @@ -9,6 +9,7 @@ import io.xpipe.core.util.StreamCharset; import java.nio.charset.Charset; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; import java.util.stream.Stream; @@ -104,8 +105,6 @@ public interface ShellDialect { String nullStdin(String command); - String getScriptPermissionsCommand(String file); - ShellDialectAskpass getAskpass(); String getSetEnvironmentVariableCommand(String variable, String value); @@ -118,7 +117,11 @@ public interface ShellDialect { CommandControl printUsernameCommand(ShellControl shellControl); - String getPrintExitCodeCommand(String prefix, String suffix); + String getPrintStartEchoCommand(String prefix); + + Optional executeRobustBootstrapOutputCommand(ShellControl shellControl, String original) throws Exception; + + String getPrintExitCodeCommand(String id, String prefix, String suffix); int assignMissingExitCode(); @@ -128,9 +131,7 @@ public interface ShellDialect { CommandBuilder getOpenScriptCommand(String file); - default void prepareCommandForShell(CommandBuilder b) {} - - String prepareTerminalInitFileOpenCommand(ShellDialect parentDialect, ShellControl sc, String file); + String prepareTerminalInitFileOpenCommand(ShellDialect parentDialect, ShellControl sc, String file, boolean exit); String runScriptCommand(ShellControl parent, String file); @@ -184,5 +185,5 @@ public interface ShellDialect { String getDisplayName(); - boolean doesEchoInput(); + boolean doesEchoInputByDefault(); } 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 a471f0d39..68893c8bd 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellDialects.java +++ b/core/src/main/java/io/xpipe/core/process/ShellDialects.java @@ -27,6 +27,7 @@ public class ShellDialects { public static ShellDialect CISCO; public static ShellDialect MIKROTIK; public static ShellDialect RBASH; + public static ShellDialect CONSTRAINED_POWERSHELL; public static ShellDialect OVH_BASTION; public static List getStartableDialects() { @@ -85,6 +86,7 @@ public class ShellDialects { CISCO = byId("cisco"); MIKROTIK = byId("mikrotik"); RBASH = byId("rbash"); + CONSTRAINED_POWERSHELL = byId("constrainedPowershell"); OVH_BASTION = byId("ovhBastion"); } } diff --git a/core/src/main/java/io/xpipe/core/process/ShellDumbMode.java b/core/src/main/java/io/xpipe/core/process/ShellDumbMode.java index 20573ed72..3faa460c2 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellDumbMode.java +++ b/core/src/main/java/io/xpipe/core/process/ShellDumbMode.java @@ -8,6 +8,8 @@ public interface ShellDumbMode { return true; } + default void throwIfUnsupported() {} + default ShellDialect getSwitchDialect() { return null; } @@ -25,6 +27,14 @@ public interface ShellDumbMode { class Unsupported implements ShellDumbMode { + private final String message; + + public Unsupported(String message) {this.message = message;} + + public void throwIfUnsupported() { + throw new UnsupportedOperationException(message); + } + @Override public boolean supportsAnyPossibleInteraction() { return false; diff --git a/core/src/main/java/io/xpipe/core/process/ShellNameStoreState.java b/core/src/main/java/io/xpipe/core/process/ShellNameStoreState.java deleted file mode 100644 index 6042b4c4c..000000000 --- a/core/src/main/java/io/xpipe/core/process/ShellNameStoreState.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.xpipe.core.process; - -import io.xpipe.core.store.DataStoreState; - -import lombok.EqualsAndHashCode; -import lombok.Value; -import lombok.experimental.SuperBuilder; -import lombok.extern.jackson.Jacksonized; - -@Value -@EqualsAndHashCode(callSuper = true) -@SuperBuilder(toBuilder = true) -@Jacksonized -public class ShellNameStoreState extends ShellStoreState { - - String shellName; - - @Override - public DataStoreState mergeCopy(DataStoreState newer) { - var n = (ShellNameStoreState) newer; - var b = toBuilder(); - mergeBuilder(n, b); - return b.shellName(useNewer(shellName, n.shellName)).build(); - } -} diff --git a/core/src/main/java/io/xpipe/core/process/ShellOpenFunction.java b/core/src/main/java/io/xpipe/core/process/ShellOpenFunction.java index ee620918c..07a4b6de3 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellOpenFunction.java +++ b/core/src/main/java/io/xpipe/core/process/ShellOpenFunction.java @@ -27,7 +27,7 @@ public interface ShellOpenFunction { @Override public CommandBuilder prepareWithInitCommand(@NonNull String command) { - throw new UnsupportedOperationException(); + return CommandBuilder.of().add(command); } }; } diff --git a/core/src/main/java/io/xpipe/core/process/ShellProperties.java b/core/src/main/java/io/xpipe/core/process/ShellProperties.java deleted file mode 100644 index b8df6d34d..000000000 --- a/core/src/main/java/io/xpipe/core/process/ShellProperties.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.xpipe.core.process; - -import lombok.Value; - -@Value -public class ShellProperties { - - ShellDialect dialect; - boolean ansiEscapes; -} 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 72dcc9efe..4f2d1360f 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellStoreState.java +++ b/core/src/main/java/io/xpipe/core/process/ShellStoreState.java @@ -19,6 +19,7 @@ public class ShellStoreState extends DataStoreState implements OsNameState { OsType.Any osType; String osName; ShellDialect shellDialect; + ShellTtyState ttyState; Boolean running; public boolean isRunning() { @@ -39,6 +40,7 @@ public class ShellStoreState extends DataStoreState implements OsNameState { b.osType(useNewer(osType, shellStoreState.getOsType())) .osName(useNewer(osName, shellStoreState.getOsName())) .shellDialect(useNewer(shellDialect, shellStoreState.getShellDialect())) + .ttyState(useNewer(ttyState, shellStoreState.getTtyState())) .running(useNewer(running, shellStoreState.getRunning())); } } diff --git a/core/src/main/java/io/xpipe/core/process/ShellTtyState.java b/core/src/main/java/io/xpipe/core/process/ShellTtyState.java new file mode 100644 index 000000000..ef1afe101 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/process/ShellTtyState.java @@ -0,0 +1,29 @@ +package io.xpipe.core.process; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +@Getter +public enum ShellTtyState { + + @JsonProperty("none") + NONE(true, false, false, true, true), + @JsonProperty("merged") + MERGED_STDERR(false, false, false, false, true), + @JsonProperty("pty") + PTY_ALLOCATED(false, true, true, false, false); + + private final boolean hasSeparateStreams; + private final boolean hasAnsiEscapes; + private final boolean echoesAllInput; + private final boolean supportsInput; + private final boolean preservesOutput; + + ShellTtyState(boolean hasSeparateStreams, boolean hasAnsiEscapes, boolean echoesAllInput, boolean supportsInput, boolean preservesOutput) { + this.hasSeparateStreams = hasSeparateStreams; + this.hasAnsiEscapes = hasAnsiEscapes; + this.echoesAllInput = echoesAllInput; + this.supportsInput = supportsInput; + this.preservesOutput = preservesOutput; + } +} 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 6073bd7be..3fb3e81e7 100644 --- a/core/src/main/java/io/xpipe/core/store/ConnectionFileSystem.java +++ b/core/src/main/java/io/xpipe/core/store/ConnectionFileSystem.java @@ -1,13 +1,13 @@ package io.xpipe.core.store; +import com.fasterxml.jackson.annotation.JsonIgnore; import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.ShellControl; - -import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Getter; import java.io.InputStream; import java.io.OutputStream; +import java.time.Duration; import java.util.List; import java.util.Optional; import java.util.stream.Stream; @@ -36,10 +36,17 @@ public class ConnectionFileSystem implements FileSystem { @Override public FileSystem open() throws Exception { shellControl.start(); - if (!shellControl.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) { + + var d = shellControl.getShellDialect().getDumbMode(); + if (!d.supportsAnyPossibleInteraction()) { shellControl.close(); - throw new UnsupportedOperationException("System shell does not support file system interaction"); + d.throwIfUnsupported(); } + + if (!shellControl.getTtyState().isPreservesOutput() || !shellControl.getTtyState().isSupportsInput()) { + throw new UnsupportedOperationException("Shell has a PTY allocated and does not support file system operations"); + } + return this; } @@ -53,10 +60,9 @@ public class ConnectionFileSystem implements FileSystem { @Override public OutputStream openOutput(String file, long totalBytes) throws Exception { - return shellControl - .getShellDialect() - .createStreamFileWriteCommand(shellControl, file, totalBytes) - .startExternalStdin(); + var cmd = shellControl.getShellDialect().createStreamFileWriteCommand(shellControl, file, totalBytes); + cmd.setExitTimeout(Duration.ofMillis(Long.MAX_VALUE)); + return cmd.startExternalStdin(); } @Override diff --git a/core/src/main/java/io/xpipe/core/store/EnabledStoreState.java b/core/src/main/java/io/xpipe/core/store/EnabledStoreState.java index b805b7e96..e91e40b84 100644 --- a/core/src/main/java/io/xpipe/core/store/EnabledStoreState.java +++ b/core/src/main/java/io/xpipe/core/store/EnabledStoreState.java @@ -19,6 +19,6 @@ public class EnabledStoreState extends DataStoreState { @Override public DataStoreState mergeCopy(DataStoreState newer) { var n = (EnabledStoreState) newer; - return EnabledStoreState.builder().enabled(n.enabled).build(); + return EnabledStoreState.builder().enabled(enabled || n.enabled).build(); } } diff --git a/core/src/main/java/io/xpipe/core/store/NetworkTunnelStore.java b/core/src/main/java/io/xpipe/core/store/NetworkTunnelStore.java index 4abbab803..7676fc2c3 100644 --- a/core/src/main/java/io/xpipe/core/store/NetworkTunnelStore.java +++ b/core/src/main/java/io/xpipe/core/store/NetworkTunnelStore.java @@ -60,7 +60,8 @@ public interface NetworkTunnelStore extends DataStore { default NetworkTunnelSession sessionChain(int local, int remotePort) throws Exception { if (!isLocallyTunneable()) { - throw new IllegalStateException("Unable to create tunnel chain as one intermediate system does not support tunneling"); + throw new IllegalStateException( + "Unable to create tunnel chain as one intermediate system does not support tunneling"); } var running = new AtomicBoolean(); 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 734e91329..a1da2051c 100644 --- a/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java +++ b/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java @@ -1,13 +1,5 @@ package io.xpipe.core.util; -import com.fasterxml.jackson.annotation.JsonIdentityInfo; -import com.fasterxml.jackson.annotation.ObjectIdGenerators; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.databind.jsontype.NamedType; -import com.fasterxml.jackson.databind.module.SimpleModule; import io.xpipe.core.dialog.BaseQueryElement; import io.xpipe.core.dialog.BusyElement; import io.xpipe.core.dialog.ChoiceElement; @@ -19,6 +11,15 @@ import io.xpipe.core.store.FilePath; import io.xpipe.core.store.LocalStore; import io.xpipe.core.store.StorePath; +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.jsontype.NamedType; +import com.fasterxml.jackson.databind.module.SimpleModule; + import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Path; 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 ccb028425..a2166823e 100644 --- a/core/src/main/java/io/xpipe/core/util/JacksonMapper.java +++ b/core/src/main/java/io/xpipe/core/util/JacksonMapper.java @@ -1,13 +1,15 @@ package io.xpipe.core.util; import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.Module; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.module.SimpleModule; import lombok.Getter; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.ServiceLoader; @@ -88,4 +90,36 @@ public class JacksonMapper { return INSTANCE; } + + public static ObjectMapper getCensored() { + if (!JacksonMapper.isInit()) { + return BASE; + } + + var c = INSTANCE.copy(); + c.registerModule(new SimpleModule() { + @Override + public void setupModule(SetupContext context) { + addSerializer(SecretValue.class, new JsonSerializer<>() { + @Override + public void serialize(SecretValue value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + gen.writeString(""); + } + + @Override + public void serializeWithType( + SecretValue value, + JsonGenerator gen, + SerializerProvider serializers, + TypeSerializer typeSer) + throws IOException { + gen.writeString(""); + } + }); + super.setupModule(context); + } + }); + return c; + } } 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 b5557c39a..09b0e8994 100644 --- a/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java +++ b/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java @@ -32,7 +32,7 @@ public class XPipeInstallation { } public static Path getLocalBeaconAuthFile() { - return Path.of(System.getProperty("java.io.tmpdir"), "xpipe_auth"); + return Path.of(System.getProperty("java.io.tmpdir"), isStaging() ? "xpipe_ptb_auth" : "xpipe_auth"); } public static String createExternalAsyncLaunchCommand( diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index 1ccf7cf3f..692574970 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -17,6 +17,7 @@ open module io.xpipe.core { requires com.fasterxml.jackson.databind; requires java.net.http; requires static lombok; + requires java.sql; uses com.fasterxml.jackson.databind.Module; uses ProcessControlProvider; diff --git a/dist/base.gradle b/dist/base.gradle index bb3f0aebd..23a3c3da5 100644 --- a/dist/base.gradle +++ b/dist/base.gradle @@ -53,31 +53,10 @@ if (org.gradle.internal.os.OperatingSystem.current().isWindows()) { debugAttachArguments + ' ' + debugArguments) debugAttach.setExecutable(true) - copy { - from "$distDir/cli" - into "$distDir/base/cli/bin" - } copy { 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" - } - - if (rootProject.fullVersion) { - file("$distDir/base/app/xpiped.exe").writable = true - exec { - commandLine "$projectDir\\tools\\sign.bat", "$distDir/base/app/xpiped.exe" - ignoreExitValue = true - } - file("$distDir/base/app/xpiped.exe").writable = false - } } } } else if (org.gradle.internal.os.OperatingSystem.current().isLinux()) { @@ -129,26 +108,6 @@ if (org.gradle.internal.os.OperatingSystem.current().isWindows()) { from "$distDir/licenses" into "$distDir/base/licenses" } - copy { - from "$distDir/cli/xpipe" - into "$distDir/base/cli/bin" - } - copy { - from "$distDir/docs/html5" - into "$distDir/base/cli/docs" - } - copy { - from "$distDir/cli/xpipe_completion" - into "$distDir/base/cli" - } - copy { - from "$distDir/docs/manpage" - into "$distDir/base/cli/man" - } - copy { - from "$projectDir/bundled_bin/$platformName" - into "$distDir/base/app/bundled" - } } } } else { @@ -163,49 +122,19 @@ if (org.gradle.internal.os.OperatingSystem.current().isWindows()) { from "$projectDir/logo/logo.icns" into "$distDir/$app/Contents/Resources/" } - copy { - from "$distDir/cli/xpipe" - into "$distDir/$app/Contents/MacOS/" - } copy { from "$distDir/licenses" into "$distDir/$app/Contents/Resources/licenses" } - copy { - from "$distDir/docs/html5" - into "$distDir/$app/Contents/Resources/cli/docs" - } - copy { - from "$distDir/docs/manpage" - into "$distDir/$app/Contents/Resources/cli/man" - } - copy { - from "$distDir/cli/xpipe_completion" - into "$distDir/$app/Contents/Resources/cli/" - } copy { from "$projectDir/fonts" into "$distDir/$app/Contents/Resources/fonts" } - copy { - from "$projectDir/bundled_bin/$platformName" - into "$distDir/$app/Contents/Resources/bundled" - } copy { from "$rootDir/lang" into "$distDir/$app/Contents/Resources/lang" } - - copy { - from "$projectDir/PkgInstaller/darwin/Resources/uninstall.sh" - into "$distDir/$app/Contents/Resources/scripts/" - } - file("$distDir/$app/Contents/Resources/scripts/uninstall.sh").text = file("$distDir/$app/Contents/Resources/scripts/uninstall.sh").text - .replaceAll("__PRODUCT__", productName) - .replaceAll("__PRODUCT_KEBAP__", kebapProductName) - .replaceAll("__VERSION__", versionString) - def debugArguments = file("$projectDir/debug/debug_arguments.txt").text.lines().map(s -> '"' + s + '"').collect(Collectors.joining( ' ')) def debugAttachArguments = file("$projectDir/debug/mac/debug_attach_arguments.txt").text.lines().map(s -> '"' + s + '"').collect( @@ -223,12 +152,6 @@ if (org.gradle.internal.os.OperatingSystem.current().isWindows()) { 'JVM-ARGS', debugAttachArguments + ' ' + debugArguments) debugAttach.setExecutable(true, false) - - if (System.getenv("MACOS_DEVELOPER_ID_APPLICATION_CERTIFICATE_NAME") != null) { - exec { - commandLine "$projectDir/misc/mac/sign_and_notarize.sh", "$projectDir", rootProject.arch.toString(), rootProject.productName - } - } } } } diff --git a/dist/build.gradle b/dist/build.gradle index 7f5f09b9d..e85d4a8d3 100644 --- a/dist/build.gradle +++ b/dist/build.gradle @@ -1,8 +1,8 @@ plugins { id 'org.beryx.jlink' version '3.0.1' - id "org.asciidoctor.jvm.convert" version "4.0.2" - id 'org.jreleaser' version '1.12.0' + id "org.asciidoctor.jvm.convert" version "4.0.3" + id 'org.jreleaser' version '1.13.1' id("com.netflix.nebula.ospackage") version "11.9.1" id 'org.gradle.crypto.checksum' version '1.4.0' id 'signing' @@ -40,11 +40,11 @@ task createChecksums(type: Checksum) { doLast { def artifactChecksumsSha256Hex = new HashMap() for (final def file in outputDirectory.get().getAsFileTree().files) { - if (file.toString().endsWith('mapping.map') || file.toString().endsWith('.asc')) { + def name = file.name.lastIndexOf('.').with {it != -1 ? file.name[0.."` +- Add option to use double clicks to open connections instead of single clicks +- Add support for foot terminal +- Fix rare null pointers and freezes in file browser +- Fix PowerShell remote session file editing not transferring file correctly +- Fix elementary terminal not launching correctly +- Fix windows jumping around when created +- Fix kubernetes not elevating correctly for non-default contexts +- Fix ohmyzsh update notification freezing shell +- Fix file browser icons being broken for links +- The Linux installers now contain application icons from multiple sizes which should increase the icon display quality +- The Linux builds now list socat as a dependency such that the kitty terminal integration will work without issues diff --git a/dist/changelogs/10.1.1_incremental.md b/dist/changelogs/10.1.1_incremental.md new file mode 100644 index 000000000..b54fcc612 --- /dev/null +++ b/dist/changelogs/10.1.1_incremental.md @@ -0,0 +1,5 @@ +- Fix terminal window closing instantly if connection failed, not showing error messages +- Fix file browser editor sometimes not applying changes +- Fix updater not doing anything when trying to install an update when downloaded installer had been deleted on a restart +- Fix xpipe CLI executable missing signature on Windows +- Fix various smaller bugs \ No newline at end of file diff --git a/dist/changelogs/10.1_incremental.md b/dist/changelogs/10.1_incremental.md index 12ceddf1f..e0b96df2d 100644 --- a/dist/changelogs/10.1_incremental.md +++ b/dist/changelogs/10.1_incremental.md @@ -1,3 +1,14 @@ +## Browser improvements + +Feedback showed that the file browser transfer pane in the bottom left was confusing and unintuitive to use. Therefore, it has now been changed to be a more straightforward download area. You can drag files into it to automatically download them. From there you can either drag them directly where you want them to be in your local desktop environment or move them into the downloads directory. + +There is now the possibility to jump to a file in a directory by typing the first few characters of its name. + +There were also a couple of bug fixes: +- Fix file transfers on Windows systems failing for files > 2GB due to overflow +- Fix remote file editing sometimes creating blank file when using vscode +- Fix file transfers failing at the end with a timeout when the connection speed was very slow + ## API additions Several new endpoints have been added to widen the capabilities for external clients: @@ -11,7 +22,10 @@ Several new endpoints have been added to widen the capabilities for external cli ## Other +- Fix xpipe not starting up when changing user on Linux +- Fix some editors and terminals not launching when using the fallback sh system shell due to missing disown command +- Fix csh sudo elevation not working - Implement various application performance improvements - Rework sidebar styling - Improve transparency styling on Windows 11 -- Fix csh sudo elevation not working \ No newline at end of file +- Add support for zed editor \ No newline at end of file diff --git a/dist/changelogs/10.2.1.md b/dist/changelogs/10.2.1.md new file mode 100644 index 000000000..25037ee47 --- /dev/null +++ b/dist/changelogs/10.2.1.md @@ -0,0 +1,67 @@ +## A new HTTP API + +There is now a new HTTP API for the XPipe daemon, which allows you to programmatically manage remote systems. You can find details and an OpenAPI specification at the new API button in the sidebar. The API page contains everything you need to get started, including code samples for various different programming languages. + +To start off, you can query connections based on various filters. With the matched connections, you can start remote shell sessions for each one and run arbitrary commands in them. You get the command exit code and output as a response, allowing you to adapt your control flow based on command outputs. Any kind of passwords and other secrets are automatically provided by XPipe when establishing a shell connection. You can also access the file systems via these shell connections to read and write remote files. + +There already exists a community made [XPipe API library for python](https://github.com/coandco/python_xpipe_client) and a [python CLI client](https://github.com/coandco/xpipe_cli). These tools allow you to interact with the API more ergonomically and can also serve as an inspiration of what you can do with the new API. If you also built a tool to interact with the XPipe API, you can let me know and I can compile a list of community development projects. + +## Service integration + +Many systems run a variety of different services such as web services and others. There is now support to detect, forward, and open the services. For example, if you are running a web service on a remote container, you can automatically forward the service port via SSH tunnels, allowing you to access these services from your local machine, e.g. in a web browser. These service tunnels can be toggled at any time. The port forwarding supports specifying a custom local target port and also works for connections with multiple intermediate systems through chained tunnels. For containers, services are automatically detected via their exposed mapped ports. For other systems, you can manually add services via their port. + +You can use an unlimited amount of local services and one active tunneled service in the community edition. + +## Script rework + +The scripting system has been reworked. There have been several issues with it being clunky and not fun to use. The new system allows you to assign each script one of multiple execution types. Based on these execution types, you can make scripts active or inactive with a toggle. If they are active, the scripts will apply in the selected use cases. There currently are these types: +- Init scripts: When enabled, they will automatically run on init in all compatible shells. This is useful for setting things like aliases consistently +- Shell scripts: When enabled, they will be copied over to the target system and put into the PATH. You can then call them in a normal shell session by their name, e.g. `myscript.sh`, also with arguments. +- File scripts: When enabled, you can call them in the file browser with the selected files as arguments. Useful to perform common actions with files + +If you have existing scripts, they will have to be manually adjusted by setting their execution types. + +## Docker improvements + +The docker integration has been updated to support docker contexts. You can use the default context in the community edition, essentially being the same as before as XPipe previously only used the default context. Support for using multiple contexts is included in the professional edition. + +There's now support for Windows docker containers running on HyperV. + +Note that old docker container connections will be removed as they are incompatible with the new version. + +## Proxmox improvements + +You can now automatically open the Proxmox dashboard website through the new service integration. This will also work with the service tunneling feature for remote servers. + +You can now open VNC sessions to Proxmox VMs. + +The Proxmox professional license requirement has been reworked to support one non-enterprise PVE node in the community edition. + +## Better connection organization + +The toggle to show only running connections will now no longer actually remove the connections internally and instead just not display them. This will reduce git vault updates and is faster in general. + +You can now order connections relative to other sibling connections. This ordering will also persist when changing the global order in the top left. + +The UI has also been streamlined to make common actions and toggles more easily accessible. + +## Other + +- The title bar on Windows will now follow the appearance theme +- Several more actions have been added for podman containers +- Support VMs for tunneling +- Searching for connections has been improved to show children as well +- There is now an AppImage portable release +- The welcome screen will now also contain the option to straight up jump to the synchronization settings +- You can now launch xpipe in another data directory with `xpipe open -d ""` +- Add option to use double clicks to open connections instead of single clicks +- Add support for foot terminal +- Fix rare null pointers and freezes in file browser +- Fix PowerShell remote session file editing not transferring file correctly +- Fix elementary terminal not launching correctly +- Fix windows jumping around when created +- Fix kubernetes not elevating correctly for non-default contexts +- Fix ohmyzsh update notification freezing shell +- Fix file browser icons being broken for links +- The Linux installers now contain application icons from multiple sizes which should increase the icon display quality +- The Linux builds now list socat as a dependency such that the kitty terminal integration will work without issues diff --git a/dist/changelogs/10.2.1_incremental.md b/dist/changelogs/10.2.1_incremental.md new file mode 100644 index 000000000..5d9289d4a --- /dev/null +++ b/dist/changelogs/10.2.1_incremental.md @@ -0,0 +1 @@ +- Fix startup issue on older x86_64 macOS systems diff --git a/dist/changelogs/10.2.2.md b/dist/changelogs/10.2.2.md new file mode 100644 index 000000000..25037ee47 --- /dev/null +++ b/dist/changelogs/10.2.2.md @@ -0,0 +1,67 @@ +## A new HTTP API + +There is now a new HTTP API for the XPipe daemon, which allows you to programmatically manage remote systems. You can find details and an OpenAPI specification at the new API button in the sidebar. The API page contains everything you need to get started, including code samples for various different programming languages. + +To start off, you can query connections based on various filters. With the matched connections, you can start remote shell sessions for each one and run arbitrary commands in them. You get the command exit code and output as a response, allowing you to adapt your control flow based on command outputs. Any kind of passwords and other secrets are automatically provided by XPipe when establishing a shell connection. You can also access the file systems via these shell connections to read and write remote files. + +There already exists a community made [XPipe API library for python](https://github.com/coandco/python_xpipe_client) and a [python CLI client](https://github.com/coandco/xpipe_cli). These tools allow you to interact with the API more ergonomically and can also serve as an inspiration of what you can do with the new API. If you also built a tool to interact with the XPipe API, you can let me know and I can compile a list of community development projects. + +## Service integration + +Many systems run a variety of different services such as web services and others. There is now support to detect, forward, and open the services. For example, if you are running a web service on a remote container, you can automatically forward the service port via SSH tunnels, allowing you to access these services from your local machine, e.g. in a web browser. These service tunnels can be toggled at any time. The port forwarding supports specifying a custom local target port and also works for connections with multiple intermediate systems through chained tunnels. For containers, services are automatically detected via their exposed mapped ports. For other systems, you can manually add services via their port. + +You can use an unlimited amount of local services and one active tunneled service in the community edition. + +## Script rework + +The scripting system has been reworked. There have been several issues with it being clunky and not fun to use. The new system allows you to assign each script one of multiple execution types. Based on these execution types, you can make scripts active or inactive with a toggle. If they are active, the scripts will apply in the selected use cases. There currently are these types: +- Init scripts: When enabled, they will automatically run on init in all compatible shells. This is useful for setting things like aliases consistently +- Shell scripts: When enabled, they will be copied over to the target system and put into the PATH. You can then call them in a normal shell session by their name, e.g. `myscript.sh`, also with arguments. +- File scripts: When enabled, you can call them in the file browser with the selected files as arguments. Useful to perform common actions with files + +If you have existing scripts, they will have to be manually adjusted by setting their execution types. + +## Docker improvements + +The docker integration has been updated to support docker contexts. You can use the default context in the community edition, essentially being the same as before as XPipe previously only used the default context. Support for using multiple contexts is included in the professional edition. + +There's now support for Windows docker containers running on HyperV. + +Note that old docker container connections will be removed as they are incompatible with the new version. + +## Proxmox improvements + +You can now automatically open the Proxmox dashboard website through the new service integration. This will also work with the service tunneling feature for remote servers. + +You can now open VNC sessions to Proxmox VMs. + +The Proxmox professional license requirement has been reworked to support one non-enterprise PVE node in the community edition. + +## Better connection organization + +The toggle to show only running connections will now no longer actually remove the connections internally and instead just not display them. This will reduce git vault updates and is faster in general. + +You can now order connections relative to other sibling connections. This ordering will also persist when changing the global order in the top left. + +The UI has also been streamlined to make common actions and toggles more easily accessible. + +## Other + +- The title bar on Windows will now follow the appearance theme +- Several more actions have been added for podman containers +- Support VMs for tunneling +- Searching for connections has been improved to show children as well +- There is now an AppImage portable release +- The welcome screen will now also contain the option to straight up jump to the synchronization settings +- You can now launch xpipe in another data directory with `xpipe open -d ""` +- Add option to use double clicks to open connections instead of single clicks +- Add support for foot terminal +- Fix rare null pointers and freezes in file browser +- Fix PowerShell remote session file editing not transferring file correctly +- Fix elementary terminal not launching correctly +- Fix windows jumping around when created +- Fix kubernetes not elevating correctly for non-default contexts +- Fix ohmyzsh update notification freezing shell +- Fix file browser icons being broken for links +- The Linux installers now contain application icons from multiple sizes which should increase the icon display quality +- The Linux builds now list socat as a dependency such that the kitty terminal integration will work without issues diff --git a/dist/changelogs/10.2.2_incremental.md b/dist/changelogs/10.2.2_incremental.md new file mode 100644 index 000000000..6fa79e493 --- /dev/null +++ b/dist/changelogs/10.2.2_incremental.md @@ -0,0 +1,6 @@ +- Fix Windows installers producing SmartScreen warning +- Fix setting to use double clicks when launching connections not working +- Fix potential stack overflow when opening VNC connections +- Fix file browser shortcuts conflicting with others and intercepting others +- Fix some broken keyboard shortcuts +- Fix certain special character key combinations being wrongfully intercepted by window, leading to a window close when typing @ on some european keyboards diff --git a/dist/changelogs/10.2.md b/dist/changelogs/10.2.md new file mode 100644 index 000000000..25037ee47 --- /dev/null +++ b/dist/changelogs/10.2.md @@ -0,0 +1,67 @@ +## A new HTTP API + +There is now a new HTTP API for the XPipe daemon, which allows you to programmatically manage remote systems. You can find details and an OpenAPI specification at the new API button in the sidebar. The API page contains everything you need to get started, including code samples for various different programming languages. + +To start off, you can query connections based on various filters. With the matched connections, you can start remote shell sessions for each one and run arbitrary commands in them. You get the command exit code and output as a response, allowing you to adapt your control flow based on command outputs. Any kind of passwords and other secrets are automatically provided by XPipe when establishing a shell connection. You can also access the file systems via these shell connections to read and write remote files. + +There already exists a community made [XPipe API library for python](https://github.com/coandco/python_xpipe_client) and a [python CLI client](https://github.com/coandco/xpipe_cli). These tools allow you to interact with the API more ergonomically and can also serve as an inspiration of what you can do with the new API. If you also built a tool to interact with the XPipe API, you can let me know and I can compile a list of community development projects. + +## Service integration + +Many systems run a variety of different services such as web services and others. There is now support to detect, forward, and open the services. For example, if you are running a web service on a remote container, you can automatically forward the service port via SSH tunnels, allowing you to access these services from your local machine, e.g. in a web browser. These service tunnels can be toggled at any time. The port forwarding supports specifying a custom local target port and also works for connections with multiple intermediate systems through chained tunnels. For containers, services are automatically detected via their exposed mapped ports. For other systems, you can manually add services via their port. + +You can use an unlimited amount of local services and one active tunneled service in the community edition. + +## Script rework + +The scripting system has been reworked. There have been several issues with it being clunky and not fun to use. The new system allows you to assign each script one of multiple execution types. Based on these execution types, you can make scripts active or inactive with a toggle. If they are active, the scripts will apply in the selected use cases. There currently are these types: +- Init scripts: When enabled, they will automatically run on init in all compatible shells. This is useful for setting things like aliases consistently +- Shell scripts: When enabled, they will be copied over to the target system and put into the PATH. You can then call them in a normal shell session by their name, e.g. `myscript.sh`, also with arguments. +- File scripts: When enabled, you can call them in the file browser with the selected files as arguments. Useful to perform common actions with files + +If you have existing scripts, they will have to be manually adjusted by setting their execution types. + +## Docker improvements + +The docker integration has been updated to support docker contexts. You can use the default context in the community edition, essentially being the same as before as XPipe previously only used the default context. Support for using multiple contexts is included in the professional edition. + +There's now support for Windows docker containers running on HyperV. + +Note that old docker container connections will be removed as they are incompatible with the new version. + +## Proxmox improvements + +You can now automatically open the Proxmox dashboard website through the new service integration. This will also work with the service tunneling feature for remote servers. + +You can now open VNC sessions to Proxmox VMs. + +The Proxmox professional license requirement has been reworked to support one non-enterprise PVE node in the community edition. + +## Better connection organization + +The toggle to show only running connections will now no longer actually remove the connections internally and instead just not display them. This will reduce git vault updates and is faster in general. + +You can now order connections relative to other sibling connections. This ordering will also persist when changing the global order in the top left. + +The UI has also been streamlined to make common actions and toggles more easily accessible. + +## Other + +- The title bar on Windows will now follow the appearance theme +- Several more actions have been added for podman containers +- Support VMs for tunneling +- Searching for connections has been improved to show children as well +- There is now an AppImage portable release +- The welcome screen will now also contain the option to straight up jump to the synchronization settings +- You can now launch xpipe in another data directory with `xpipe open -d ""` +- Add option to use double clicks to open connections instead of single clicks +- Add support for foot terminal +- Fix rare null pointers and freezes in file browser +- Fix PowerShell remote session file editing not transferring file correctly +- Fix elementary terminal not launching correctly +- Fix windows jumping around when created +- Fix kubernetes not elevating correctly for non-default contexts +- Fix ohmyzsh update notification freezing shell +- Fix file browser icons being broken for links +- The Linux installers now contain application icons from multiple sizes which should increase the icon display quality +- The Linux builds now list socat as a dependency such that the kitty terminal integration will work without issues diff --git a/dist/changelogs/10.2_incremental.md b/dist/changelogs/10.2_incremental.md new file mode 100644 index 000000000..d5e19b4ba --- /dev/null +++ b/dist/changelogs/10.2_incremental.md @@ -0,0 +1,31 @@ +## File browser improvements + +- Add right click context menu to browser tabs +- Add ability to select tabs with function keys, e.g. F1, F2, ... +- Add ability to cycle between tabs with CTRL+TAB and CTRL+SHIFT+TAB +- Fix some keyboard shortcuts being broken +- Fix pressing enter on rename also opening file +- Fix right click not opening context menu in empty directory +- Fix shell opener in navigation bar being broken, so you can now run programs and shells again from the navigation bar similar to Windows explorer +- There is now an always visible loading indicator when a tab is being opened +- Add timeout to file selection when typing a file name that was not found +- Improve flow of file selection by when typing its name +- Remove limitation of only being able to open one system at the time while it is loading + +## Other + +- Rework UI to be more compact and show more connections +- Implement native window styling on macOS +- Add support for VNC RSA-AES authentication schemes, allowing to connect to more types of VNC servers +- Services can now be opened in a browser using either HTTP or HTTPs +- You can now create shortcuts to automatically forward and open services in a browser +- Fix docker containers in some cases not persisting, leaving invalid orphan connections behind on the bottom +- Fix connection failures to proxmox VMs that have additional custom network interfaces +- Fix window not saving maximized state on restart +- Don't modify git URLs anymore to fix sync with certain providers like azure +- Improve git remote connection error messages +- Replace system tray mode with background mode on Linux +- Improve description for service groups +- Publish API libraries to maven central +- Show warning when launching PowerShell in constrained language mode +- Fix rare NullPointers when migrating an old vault diff --git a/dist/changelogs/11.0.md b/dist/changelogs/11.0.md new file mode 100644 index 000000000..bca13a08c --- /dev/null +++ b/dist/changelogs/11.0.md @@ -0,0 +1,49 @@ +## TTYs and PTYs + +Up until now, if you added a connection that always allocated pty, XPipe would complain about a missing stderr. +In XPipe 11, there has been a ground up rework of the shell initialization code which will in theory allow for better handling of these cases. +They are not fully supported yet and have some issues, but should work better. + +The main concern here is to verify that the existing normal shell implementation still works as before and there were no bugs introduced by this rework. + +## Teleport support + +There is now support to add your teleport connections that are available via tsh. + +## Profiles + +You can now create multiple user profiles in the settings menu. + +This will create desktop shortcuts that you can use to start XPipe with different profiles active. + +## Serial connection support + +There is now support to add serial connections. + +## Scripting improvements + +The scripting system has been reworked in order to make it more intuitive and powerful. + +The script execution types have been renamed, the documentation has been improved, and a new execution type has been added. +The new runnable execution type will allow you to call a script from the connection hub directly in a dropdown for each connection when the script is active. +This will also replace the current terminal command functionality, which has been removed. + +Any file browser scripts are now grouped by the scripts groups they are in, improving the overview when having many file browser scripts. +Furthermore, you can now launch these scripts in the file browser either in the background if they are quiet or in a terminal if they are intended to be interactive. +When multiple files are selected, a script is now called only once with all the selected files as arguments. + +## Other + +- Rework state information display for proxmox VMs +- Fix git sync freezing when using key with passphrase on modern ssh clients +- Fix git sync restarting daemon after exit when using key with passphrase +- Fix terminal exit not working properly in fish +- Fix renaming a connection clearing all state information +- Fix script enabled status being wrong after editing an enabled script +- Fix download move operation failing when moving a directory that already existed in the downloads folder +- Fix some scrollbars are necessarily showing +- Automatically fill identity file for ssh config wildcard keys as well +- Improve error messages when system interaction was disabled for a system +- Don't show git all compatibility warnings on minor version updates +- Enable ZGC on Linux and macOS +- Some small appearance fixes \ No newline at end of file diff --git a/dist/jpackage.gradle b/dist/jpackage.gradle index 62ae673d3..993e15018 100644 --- a/dist/jpackage.gradle +++ b/dist/jpackage.gradle @@ -54,7 +54,8 @@ jlink { '--no-header-files', '--no-man-pages', '--include-locales', "${String.join(",", languages)}", - '--compress', 'zip-9' + '--compress', 'zip-9', + '--ignore-signing-information' ] if (org.gradle.internal.os.OperatingSystem.current().isLinux()) { @@ -118,7 +119,7 @@ task prepareMacOSInfo(type: DefaultTask) { doLast { file("${project.layout.buildDirectory.get()}/macos_resources").mkdirs() copy { - from replaceVariablesInFile("$projectDir/misc/mac/Info.plist", + from replaceVariablesInFile("$projectDir/jpackage/Info.plist", Map.of('__NAME__', rootProject.productName, '__VERSION__', diff --git a/dist/jpackage/Info.plist b/dist/jpackage/Info.plist new file mode 100644 index 000000000..5557a5efb --- /dev/null +++ b/dist/jpackage/Info.plist @@ -0,0 +1,48 @@ + + + + + CFBundleAllowMixedLocalizations + + CFBundleDevelopmentRegion + English + CFBundleExecutable + xpiped + CFBundleIconFile + xpiped.icns + CFBundleIdentifier + __BUNDLE__ + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + __NAME__ + CFBundlePackageType + APPL + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleURLName + io.xpipe.URLScheme + CFBundleURLSchemes + + xpipe + ssh + + + + LSApplicationCategoryType + public.app-category.developer-tools + LSMinimumSystemVersion + 10.11 + NSHighResolutionCapable + true + NSHumanReadableCopyright + Copyright (C) 2024 + CFBundleShortVersionString + __VERSION__ + CFBundleVersion + __VERSION__ + + diff --git a/dist/licenses/commons-lang.license b/dist/licenses/commons-lang.license new file mode 100644 index 000000000..f433b1a53 --- /dev/null +++ b/dist/licenses/commons-lang.license @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/dist/licenses/commons-lang.properties b/dist/licenses/commons-lang.properties new file mode 100644 index 000000000..7c09cfa09 --- /dev/null +++ b/dist/licenses/commons-lang.properties @@ -0,0 +1,4 @@ +name=Commons Lang +version=3.16.0 +license=Apache License 2.0 +link=https://commons.apache.org/proper/commons-lang/ \ No newline at end of file diff --git a/dist/licenses/jackson.properties b/dist/licenses/jackson.properties index f59e60d6a..96ea13235 100644 --- a/dist/licenses/jackson.properties +++ b/dist/licenses/jackson.properties @@ -1,4 +1,4 @@ name=Jackson Databind -version=2.17.1 +version=2.17.2 license=Apache License 2.0 link=https://github.com/FasterXML/jackson-databind \ No newline at end of file diff --git a/dist/licenses/sentry.properties b/dist/licenses/sentry.properties index 642b705ae..638c7b8a4 100644 --- a/dist/licenses/sentry.properties +++ b/dist/licenses/sentry.properties @@ -1,4 +1,4 @@ name=Sentry Java -version=7.6.0 +version=7.13.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 cecb18920..2dc87d2c4 100644 --- a/dist/licenses/slf4j.properties +++ b/dist/licenses/slf4j.properties @@ -1,4 +1,4 @@ name=SLF4J -version=2.0.13 +version=2.0.15 license=MIT License link=https://www.slf4j.org/ \ No newline at end of file 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 f01d8e95a..806e9eb60 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 @@ -32,12 +32,6 @@ 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"); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchStoreAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchStoreAction.java index 450eb5a94..9ebaa088a 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchStoreAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchStoreAction.java @@ -90,7 +90,7 @@ public class LaunchStoreAction implements ActionProvider { @Override public void execute() throws Exception { - var storeName = DataStorage.get().getStoreDisplayName(entry); + var storeName = DataStorage.get().getStoreEntryDisplayName(entry); if (entry.getStore() instanceof ShellStore s) { TerminalLauncher.open(entry, storeName, null, ScriptStore.controlWithDefaultScripts(s.control())); return; diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/RunScriptActionMenu.java b/ext/base/src/main/java/io/xpipe/ext/base/action/RunScriptActionMenu.java new file mode 100644 index 000000000..8380aa77b --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/RunScriptActionMenu.java @@ -0,0 +1,289 @@ +package io.xpipe.ext.base.action; + +import io.xpipe.app.comp.store.StoreViewState; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.ext.ActionProvider; +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.util.TerminalLauncher; +import io.xpipe.core.process.ShellStoreState; +import io.xpipe.core.store.ShellStore; +import io.xpipe.ext.base.script.ScriptHierarchy; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ObservableValue; +import lombok.Value; + +import java.util.List; + +public class RunScriptActionMenu implements ActionProvider { + + @Value + private static class TerminalRunActionProvider implements ActionProvider { + + ScriptHierarchy hierarchy; + + @Value + private class Action implements ActionProvider.Action { + + DataStoreEntryRef shellStore; + + @Override + public void execute() throws Exception { + try (var sc = shellStore.getStore().control().start()) { + var script = hierarchy.getLeafBase().getStore().assembleScriptChain(sc); + TerminalLauncher.open( + shellStore.getEntry(), + hierarchy.getLeafBase().get().getName() + " - " + shellStore.get().getName(), + null, + sc.command(script)); + } + } + } + + @Override + public LeafDataStoreCallSite getLeafDataStoreCallSite() { + return new LeafDataStoreCallSite() { + @Override + public Action createAction(DataStoreEntryRef store) { + return new Action(store); + } + + @Override + public ObservableValue getName(DataStoreEntryRef store) { + var t = AppPrefs.get().terminalType().getValue(); + return AppI18n.observable( + "executeInTerminal", + t != null ? t.toTranslatedString().getValue() : "?"); + } + + @Override + public String getIcon(DataStoreEntryRef store) { + return "mdi2d-desktop-mac"; + } + + @Override + public Class getApplicableClass() { + return ShellStore.class; + } + }; + } + } + + @Value + private static class BackgroundRunActionProvider implements ActionProvider { + + ScriptHierarchy hierarchy; + + @Value + private class Action implements ActionProvider.Action { + + DataStoreEntryRef shellStore; + + @Override + public void execute() throws Exception { + try (var sc = shellStore.getStore().control().start()) { + var script = hierarchy.getLeafBase().getStore().assembleScriptChain(sc); + sc.command(script).execute(); + } + } + } + + @Override + public LeafDataStoreCallSite getLeafDataStoreCallSite() { + return new LeafDataStoreCallSite() { + @Override + public Action createAction(DataStoreEntryRef store) { + return new Action(store); + } + + @Override + public ObservableValue getName(DataStoreEntryRef store) { + return AppI18n.observable("executeInBackground"); + } + + @Override + public String getIcon(DataStoreEntryRef store) { + return "mdi2f-flip-to-back"; + } + + @Override + public Class getApplicableClass() { + return ShellStore.class; + } + }; + } + } + + @Value + private static class ScriptActionProvider implements ActionProvider { + + ScriptHierarchy hierarchy; + + private BranchDataStoreCallSite getLeafSite() { + return new BranchDataStoreCallSite() { + + @Override + public Class getApplicableClass() { + return ShellStore.class; + } + + @Override + public ObservableValue getName(DataStoreEntryRef store) { + return new SimpleStringProperty(hierarchy.getBase().get().getName()); + } + + @Override + public boolean isDynamicallyGenerated() { + return true; + } + + @Override + public String getIcon(DataStoreEntryRef store) { + return "mdi2p-play-box-multiple-outline"; + } + + @Override + public List getChildren(DataStoreEntryRef store) { + return List.of(new TerminalRunActionProvider(hierarchy), new BackgroundRunActionProvider(hierarchy)); + } + }; + } + + public BranchDataStoreCallSite getBranchDataStoreCallSite() { + if (hierarchy.isLeaf()) { + return getLeafSite(); + } + + return new BranchDataStoreCallSite() { + + @Override + public Class getApplicableClass() { + return ShellStore.class; + } + + @Override + public ObservableValue getName(DataStoreEntryRef store) { + return new SimpleStringProperty(hierarchy.getBase().get().getName()); + } + + @Override + public boolean isDynamicallyGenerated() { + return true; + } + + @Override + public String getIcon(DataStoreEntryRef store) { + return "mdi2p-play-box-multiple-outline"; + } + + @Override + public List getChildren(DataStoreEntryRef store) { + return hierarchy.getChildren().stream().map(c -> new ScriptActionProvider(c)).toList(); + } + }; + } + } + + private static class NoScriptsActionProvider implements ActionProvider { + + private static class Action implements ActionProvider.Action { + + @Override + public void execute() throws Exception { + StoreViewState.get().getAllScriptsCategory().select(); + } + } + + @Override + public LeafDataStoreCallSite getLeafDataStoreCallSite() { + return new LeafDataStoreCallSite() { + @Override + public Action createAction(DataStoreEntryRef store) { + return new Action(); + } + + @Override + public ObservableValue getName(DataStoreEntryRef store) { + return AppI18n.observable("noScriptsAvailable"); + } + + @Override + public String getIcon(DataStoreEntryRef store) { + return "mdi2i-image-filter-none"; + } + + @Override + public Class getApplicableClass() { + return ShellStore.class; + } + }; + } + } + + @Override + public BranchDataStoreCallSite getBranchDataStoreCallSite() { + return new BranchDataStoreCallSite() { + + @Override + public Class getApplicableClass() { + return ShellStore.class; + } + + @Override + public boolean isMajor(DataStoreEntryRef o) { + return true; + } + + @Override + public ObservableValue getName(DataStoreEntryRef store) { + return AppI18n.observable("runScript"); + } + + @Override + public boolean isDynamicallyGenerated() { + return true; + } + + @Override + public String getIcon(DataStoreEntryRef store) { + return "mdi2p-play-box-multiple-outline"; + } + + @Override + public boolean isApplicable(DataStoreEntryRef o) { + var state = o.getEntry().getStorePersistentState(); + if (!(state instanceof ShellStoreState shellStoreState) || shellStoreState.getShellDialect() == null) { + return false; + } + + return true; + } + + @Override + public List getChildren(DataStoreEntryRef store) { + var state = store.getEntry().getStorePersistentState(); + if (!(state instanceof ShellStoreState shellStoreState) || shellStoreState.getShellDialect() == null) { + return List.of(new NoScriptsActionProvider()); + } + + var hierarchy = ScriptHierarchy.buildEnabledHierarchy(ref -> { + if (!ref.getStore().isRunnableScript()) { + return false; + } + + if (!ref.getStore().isCompatible(shellStoreState.getShellDialect())) { + return false; + } + + return true; + }); + var list = hierarchy.getChildren().stream().map(c -> new ScriptActionProvider(c)).toList(); + if (list.isEmpty()) { + return List.of(new NoScriptsActionProvider()); + } else { + return list; + } + } + }; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/SampleStoreAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/SampleStoreAction.java index 27b2db2d5..77c8bc5eb 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/SampleStoreAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/SampleStoreAction.java @@ -10,13 +10,11 @@ import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellDialects; import io.xpipe.core.store.LocalStore; import io.xpipe.core.store.ShellStore; - import javafx.beans.value.ObservableValue; - import lombok.Value; import java.io.BufferedReader; -import java.io.InputStreamReader; +import java.io.StringReader; public class SampleStoreAction implements ActionProvider { @@ -83,23 +81,14 @@ public class SampleStoreAction implements ActionProvider { sc.executeSimpleStringCommand(sc.getShellDialect().getEchoCommand("hello!", false)); // You can also implement custom handling for more complex commands - try (CommandControl cc = sc.command("ls").start()) { - // Discard stderr - cc.discardErr(); - - // Read the stdout lines as a stream - BufferedReader reader = new BufferedReader(new InputStreamReader(cc.getStdout(), cc.getCharset())); - // We don't have to close this stream here, that will be automatically done by the command control - // after the try-with block - reader.lines().filter(s -> !s.isBlank()).forEach(s -> { - System.out.println(s); - }); - - // Waits for command completion and returns exit code - if (cc.getExitCode() != 0) { - // Handle failure - } - } + var lsOut = sc.command("ls").readStdoutOrThrow(); + // Read the stdout lines as a stream + BufferedReader reader = new BufferedReader(new StringReader(lsOut)); + // We don't have to close this stream here, that will be automatically done by the command control + // after the try-with block + reader.lines().filter(s -> !s.isBlank()).forEach(s -> { + System.out.println(s); + }); // Commands can also be more complex and span multiple lines. // In this case, XPipe will internally write a command to a script file and then execute the script diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/ScanStoreAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/ScanStoreAction.java index 7064ff551..a236f930c 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/ScanStoreAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/ScanStoreAction.java @@ -7,9 +7,7 @@ import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.util.ScanAlert; import io.xpipe.core.process.ShellStoreState; import io.xpipe.core.store.ShellStore; - import javafx.beans.value.ObservableValue; - import lombok.Value; public class ScanStoreAction implements ActionProvider { @@ -67,7 +65,9 @@ public class ScanStoreAction implements ActionProvider { @Override public void execute() { - ScanAlert.showAsync(entry); + if (entry == null || entry.getStore() instanceof ShellStore) { + ScanAlert.showForShellStore(entry); + } } } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/MultiExecuteAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/MultiExecuteAction.java index bcf71bd9c..5bea9645f 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/MultiExecuteAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/MultiExecuteAction.java @@ -9,7 +9,6 @@ import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.TerminalLauncher; import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.ShellControl; - import javafx.beans.value.ObservableValue; import java.util.List; @@ -28,14 +27,18 @@ public abstract class MultiExecuteAction implements BranchAction { model.withShell( pc -> { for (BrowserEntry entry : entries) { + var cmd = pc.command(createCommand(pc, model, entry)); + if (cmd == null) { + continue; + } + TerminalLauncher.open( model.getEntry().getEntry(), entry.getRawFileEntry().getName(), model.getCurrentDirectory() != null ? model.getCurrentDirectory() .getPath() - : null, - pc.command(createCommand(pc, model, entry))); + : null, cmd); } }, false); @@ -61,7 +64,12 @@ public abstract class MultiExecuteAction implements BranchAction { model.withShell( pc -> { for (BrowserEntry entry : entries) { - pc.command(createCommand(pc, model, entry)) + var cmd = createCommand(pc, model, entry); + if (cmd == null) { + continue; + } + + pc.command(cmd) .withWorkingDirectory(model.getCurrentDirectory() .getPath()) .execute(); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/MultiExecuteSelectionAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/MultiExecuteSelectionAction.java new file mode 100644 index 000000000..ee166754e --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/MultiExecuteSelectionAction.java @@ -0,0 +1,77 @@ +package io.xpipe.ext.base.browser; + +import io.xpipe.app.browser.action.BranchAction; +import io.xpipe.app.browser.action.LeafAction; +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.browser.fs.OpenFileSystemModel; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.util.TerminalLauncher; +import io.xpipe.core.process.CommandBuilder; +import io.xpipe.core.process.ShellControl; +import javafx.beans.value.ObservableValue; + +import java.util.List; + +public abstract class MultiExecuteSelectionAction implements BranchAction { + + protected abstract CommandBuilder createCommand(ShellControl sc, OpenFileSystemModel model, List entries); + + protected abstract String getTerminalTitle(); + + @Override + public List getBranchingActions(OpenFileSystemModel model, List entries) { + return List.of( + new LeafAction() { + + @Override + public void execute(OpenFileSystemModel model, List entries) { + model.withShell( + pc -> { + var cmd = pc.command(createCommand(pc, model, entries)); + TerminalLauncher.open( + model.getEntry().getEntry(), + getTerminalTitle(), + model.getCurrentDirectory() != null + ? model.getCurrentDirectory() + .getPath() + : null, cmd); + }, + false); + } + + @Override + public ObservableValue getName(OpenFileSystemModel model, List entries) { + var t = AppPrefs.get().terminalType().getValue(); + return AppI18n.observable( + "executeInTerminal", + t != null ? t.toTranslatedString().getValue() : "?"); + } + + @Override + public boolean isApplicable(OpenFileSystemModel model, List entries) { + return AppPrefs.get().terminalType().getValue() != null; + } + }, + new LeafAction() { + + @Override + public void execute(OpenFileSystemModel model, List entries) { + model.withShell( + pc -> { + var cmd = createCommand(pc, model, entries); + pc.command(cmd) + .withWorkingDirectory(model.getCurrentDirectory() + .getPath()) + .execute(); + }, + false); + } + + @Override + public ObservableValue getName(OpenFileSystemModel model, List entries) { + return AppI18n.observable("executeInBackground"); + } + }); + } +} 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 64ddec426..dc07776da 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 @@ -48,6 +48,7 @@ public class OpenFileDefaultAction implements LeafAction { @Override public boolean isApplicable(OpenFileSystemModel model, List entries) { - return entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE); + return model.getFileList().getEditing().getValue() == null + && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE); } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/ToFileCommandAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/ToFileCommandAction.java index a9a1de88b..08001e17c 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/ToFileCommandAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/ToFileCommandAction.java @@ -16,12 +16,10 @@ public abstract class ToFileCommandAction implements LeafAction, ApplicationPath ShellControl sc = model.getFileSystem().getShell().orElseThrow(); for (BrowserEntry entry : entries) { var command = createCommand(model, entry); - try (var cc = sc.command(command) + var out = sc.command(command) .withWorkingDirectory(model.getCurrentDirectory().getPath()) - .start()) { - cc.discardErr(); - FileOpener.openCommandOutput(entry.getFileName(), entry, cc); - } + .readStdoutOrThrow(); + FileOpener.openReadOnlyString(out); } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopEnvironmentStore.java b/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopEnvironmentStore.java index f0f86d8ae..a4b07e363 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopEnvironmentStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopEnvironmentStore.java @@ -51,10 +51,10 @@ public class DesktopEnvironmentStore extends JacksonizedValue var f = ScriptStore.flatten(scripts); var filtered = f.stream() .filter(simpleScriptStore -> - simpleScriptStore.getMinimumDialect().isCompatibleTo(dialect)) + simpleScriptStore.getStore().getMinimumDialect().isCompatibleTo(dialect)) .toList(); var initCommands = new ArrayList<>(filtered.stream() - .map(simpleScriptStore -> simpleScriptStore.getCommands()) + .map(simpleScriptStore -> simpleScriptStore.getStore().getCommands()) .toList()); if (initScript != null) { initCommands.add(initScript); @@ -97,7 +97,7 @@ public class DesktopEnvironmentStore extends JacksonizedValue var scriptFile = base.getStore().createScript(dialect, toExecute); var launchScriptFile = base.getStore() .createScript( - dialect, dialect.prepareTerminalInitFileOpenCommand(dialect, null, scriptFile.toString())); + dialect, dialect.prepareTerminalInitFileOpenCommand(dialect, null, scriptFile.toString(), false)); var launchConfig = new ExternalTerminalType.LaunchConfiguration(null, name, name, launchScriptFile, dialect); base.getStore().runDesktopScript(name, launchCommand.apply(launchConfig)); } 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 8d8e043de..ecda4e865 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 @@ -61,17 +61,25 @@ public enum PredefinedScriptStore { .commands(file("starship_powershell.ps1")) .initScript(true) .build()), - APT_UPDATE("Apt update", () -> SimpleScriptStore.builder() + APT_UPDATE("Apt upgrade", () -> SimpleScriptStore.builder() .group(PredefinedScriptGroup.MANAGEMENT.getEntry()) .minimumDialect(ShellDialects.SH) - .commands(file(("apt_update.sh"))) + .commands(file(("apt_upgrade.sh"))) .shellScript(true) + .runnableScript(true) .build()), REMOVE_CR("CRLF to LF", () -> SimpleScriptStore.builder() .group(PredefinedScriptGroup.FILES.getEntry()) .minimumDialect(ShellDialects.SH) .commands(file(("crlf_to_lf.sh"))) .fileScript(true) + .shellScript(true) + .build()), + DIFF("Diff", () -> SimpleScriptStore.builder() + .group(PredefinedScriptGroup.FILES.getEntry()) + .minimumDialect(ShellDialects.SH) + .commands(file(("diff.sh"))) + .fileScript(true) .build()); private final String name; diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/RunScriptAction.java b/ext/base/src/main/java/io/xpipe/ext/base/script/RunScriptAction.java index 0838059ef..64a30b3b3 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/RunScriptAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/RunScriptAction.java @@ -2,27 +2,21 @@ package io.xpipe.ext.base.script; import io.xpipe.app.browser.action.BranchAction; import io.xpipe.app.browser.action.BrowserAction; -import io.xpipe.app.browser.action.LeafAction; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.browser.session.BrowserSessionModel; import io.xpipe.app.core.AppI18n; -import io.xpipe.app.issue.ErrorEvent; -import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.util.ScriptHelper; +import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.ShellControl; -import io.xpipe.core.store.FilePath; - +import io.xpipe.ext.base.browser.MultiExecuteSelectionAction; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; import javafx.scene.Node; - import org.kordamp.ikonli.javafx.FontIcon; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; public class RunScriptAction implements BrowserAction, BranchAction { @@ -43,69 +37,82 @@ public class RunScriptAction implements BrowserAction, BranchAction { @Override public boolean isApplicable(OpenFileSystemModel model, List entries) { - var sc = model.getFileSystem().getShell().orElseThrow(); return model.getBrowserModel() instanceof BrowserSessionModel - && !getInstances(sc).isEmpty(); - } - - private Map getInstances(ShellControl sc) { - var scripts = ScriptStore.flatten(ScriptStore.getDefaultEnabledScripts()); - var map = new LinkedHashMap(); - for (SimpleScriptStore script : scripts) { - if (!script.isFileScript()) { - continue; - } - - if (!script.isCompatible(sc)) { - continue; - } - - var entry = DataStorage.get().getStoreEntryIfPresent(script, true); - if (entry.isPresent()) { - map.put(entry.get().getName(), script); - } - } - return map; + && !createActionForScriptHierarchy(model, entries).isEmpty(); } @Override - public List getBranchingActions(OpenFileSystemModel model, List entries) { - var sc = model.getFileSystem().getShell().orElseThrow(); - var scripts = getInstances(sc); - List actions = scripts.entrySet().stream() - .map(e -> { - return new LeafAction() { - @Override - public void execute(OpenFileSystemModel model, List entries) throws Exception { - var args = entries.stream() - .map(browserEntry -> new FilePath(browserEntry - .getRawFileEntry() - .getPath()) - .quoteIfNecessary()) - .collect(Collectors.joining(" ")); - execute(model, args); - } - - private void execute(OpenFileSystemModel model, String args) throws Exception { - if (model.getBrowserModel() instanceof BrowserSessionModel bm) { - var content = e.getValue().assemble(sc); - var script = ScriptHelper.createExecScript(sc, content); - try { - sc.executeSimpleCommand(sc.getShellDialect().runScriptCommand(sc, script.toString()) + " " + args); - } catch (Exception ex) { - throw ErrorEvent.expected(ex); - } - } - } - - @Override - public ObservableValue getName(OpenFileSystemModel model, List entries) { - return new SimpleStringProperty(e.getKey()); - } - }; - }) - .map(leafAction -> (LeafAction) leafAction) - .toList(); + public List getBranchingActions(OpenFileSystemModel model, List entries) { + var actions = createActionForScriptHierarchy(model, entries); return actions; } + + private List createActionForScriptHierarchy(OpenFileSystemModel model, List selected) { + var sc = model.getFileSystem().getShell().orElseThrow(); + var hierarchy = ScriptHierarchy.buildEnabledHierarchy(ref -> { + if (!ref.getStore().isFileScript()) { + return false; + } + + if (!ref.getStore().isCompatible(sc)) { + return false; + } + return true; + }); + return createActionForScriptHierarchy(model, hierarchy).getBranchingActions(model, selected); + } + + private BranchAction createActionForScriptHierarchy(OpenFileSystemModel model, ScriptHierarchy hierarchy) { + if (hierarchy.isLeaf()) { + return createActionForScript(model, hierarchy.getLeafBase()); + } + + var list = hierarchy.getChildren().stream().map(c -> createActionForScriptHierarchy(model, c)).toList(); + return new BranchAction() { + @Override + public List getBranchingActions(OpenFileSystemModel model, List entries) { + return list; + } + + @Override + public ObservableValue getName(OpenFileSystemModel model, List entries) { + var b = hierarchy.getBase(); + return new SimpleStringProperty(b != null ? b.get().getName() : null); + } + }; + } + + private BranchAction createActionForScript(OpenFileSystemModel model, DataStoreEntryRef ref) { + return new MultiExecuteSelectionAction() { + + @Override + public ObservableValue getName(OpenFileSystemModel model, List entries) { + return new SimpleStringProperty(ref.get().getName()); + } + + @Override + protected CommandBuilder createCommand(ShellControl sc, OpenFileSystemModel model, List selected) { + if (!(model.getBrowserModel() instanceof BrowserSessionModel)) { + return null; + } + + var content = ref.getStore().assembleScriptChain(sc); + var script = ScriptHelper.createExecScript(sc, content); + var builder = CommandBuilder.of().add(sc.getShellDialect().runScriptCommand(sc, script.toString())); + selected.stream() + .map(browserEntry -> browserEntry + .getRawFileEntry() + .getPath()) + .forEach(s -> { + builder.addFile(s); + }); + return builder; + } + + @Override + protected String getTerminalTitle() { + return ref.get().getName() + " - " + model.getName(); + } + }; + } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStore.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStore.java index c7578bd7b..f29e94c8b 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStore.java @@ -25,7 +25,7 @@ public class ScriptGroupStore extends ScriptStore implements GroupStore all) { + protected void queryFlattenedScripts(LinkedHashSet> all) { getEffectiveScripts().forEach(simpleScriptStore -> { simpleScriptStore.getStore().queryFlattenedScripts(all); }); @@ -35,6 +35,7 @@ public class ScriptGroupStore extends ScriptStore implements GroupStore> getEffectiveScripts() { var self = getSelfEntry(); return DataStorage.get().getDeepStoreChildren(self).stream() + .filter(entry -> entry.getValidity().isUsable()) .map(dataStoreEntry -> dataStoreEntry.ref()) .toList(); } 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 ebbe867f0..cbf311c0b 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 @@ -2,6 +2,7 @@ package io.xpipe.ext.base.script; import io.xpipe.app.comp.base.SystemStateComp; import io.xpipe.app.comp.store.StoreEntryWrapper; +import io.xpipe.app.comp.store.StoreSection; import io.xpipe.app.comp.store.StoreViewState; import io.xpipe.app.ext.*; import io.xpipe.app.fxcomps.Comp; @@ -76,8 +77,9 @@ public class ScriptGroupStoreProvider implements EnabledStoreProvider, DataStore } @Override - public ObservableValue informationString(StoreEntryWrapper wrapper) { - ScriptGroupStore scriptStore = wrapper.getEntry().getStore().asNeeded(); + public ObservableValue informationString(StoreSection section) { + ScriptGroupStore scriptStore = + section.getWrapper().getEntry().getStore().asNeeded(); return new SimpleStringProperty(scriptStore.getDescription()); } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptHierarchy.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptHierarchy.java new file mode 100644 index 000000000..8a198ac1a --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptHierarchy.java @@ -0,0 +1,108 @@ +package io.xpipe.ext.base.script; + +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntryRef; +import lombok.Value; + +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.function.Predicate; + +@Value +public class ScriptHierarchy { + + public static ScriptHierarchy buildEnabledHierarchy(Predicate> include) { + var all = new HashSet<>(ScriptStore.getEnabledScripts()); + + // Add individual children of groups + // This is not recursive + for (DataStoreEntryRef ref : new HashSet<>(all)) { + if (ref.getStore() instanceof ScriptGroupStore groupStore) { + all.addAll(groupStore.getEffectiveScripts()); + } + } + + // Add parents + for (DataStoreEntryRef ref : new HashSet<>(all)) { + var current = ref; + while (true) { + var parent = DataStorage.get().getDefaultDisplayParent(current.get()); + if (parent.isPresent()) { + all.add(parent.get().ref()); + current = parent.get().ref(); + } else { + break; + } + } + } + + var top = all.stream().filter(ref -> { + var parent = DataStorage.get().getDefaultDisplayParent(ref.get()); + return parent.isEmpty(); + }).toList(); + + var mapped = top.stream() + .map(ref -> buildHierarchy(ref, check -> { + if (!(check.getStore() instanceof SimpleScriptStore)) { + return true; + } + + if (!include.test(check.get().ref())) { + return false; + } + + return all.contains(check); + })) + .map(hierarchy -> condenseHierarchy(hierarchy)) + .filter(hierarchy -> hierarchy.show()) + .sorted(Comparator.comparing(scriptHierarchy -> scriptHierarchy.getBase().get().getName().toLowerCase())) + .toList(); + return condenseHierarchy(new ScriptHierarchy(null, mapped)); + } + + private static ScriptHierarchy buildHierarchy(DataStoreEntryRef ref, Predicate> include) { + if (ref.getStore() instanceof ScriptGroupStore groupStore) { + var children = groupStore.getEffectiveScripts().stream().filter(include) + .map(c -> buildHierarchy(c, include)) + .filter(hierarchy -> hierarchy.show()) + .sorted(Comparator.comparing(scriptHierarchy -> scriptHierarchy.getBase().get().getName().toLowerCase())) + .toList(); + return new ScriptHierarchy(ref, children); + } else { + return new ScriptHierarchy(ref, List.of()); + } + } + + + public static ScriptHierarchy condenseHierarchy(ScriptHierarchy hierarchy) { + var children = hierarchy.getChildren().stream() + .map(c -> condenseHierarchy(c)) + .toList(); + if (children.size() == 1 && !children.getFirst().isLeaf()) { + var nestedChildren = children.getFirst().getChildren(); + return new ScriptHierarchy(hierarchy.getBase(), nestedChildren); + } else { + return new ScriptHierarchy(hierarchy.getBase(), children); + } + } + + DataStoreEntryRef base; + List children; + + public boolean show() { + return isLeaf() || !isEmptyBranch(); + } + + public boolean isEmptyBranch() { + return (base == null || base.getStore() instanceof ScriptGroupStore) && children.isEmpty(); + } + + public boolean isLeaf() { + return base != null && base.getStore() instanceof SimpleScriptStore && children.isEmpty(); + } + + public DataStoreEntryRef getLeafBase() { + return base.get().ref(); + } +} 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 9cb2740c3..dfcf0279e 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 @@ -34,7 +34,7 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore, protected final String description; public static ShellControl controlWithDefaultScripts(ShellControl pc) { - return controlWithScripts(pc, getDefaultEnabledScripts()); + return controlWithScripts(pc, getEnabledScripts()); } public static ShellControl controlWithScripts( @@ -46,10 +46,10 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore, } var initFlattened = flatten(enabledScripts).stream() - .filter(store -> store.isInitScript()) + .filter(store -> store.getStore().isInitScript()) .toList(); var bringFlattened = flatten(enabledScripts).stream() - .filter(store -> store.isShellScript()) + .filter(store -> store.getStore().isShellScript()) .toList(); // Optimize if we have nothing to do @@ -58,7 +58,7 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore, } initFlattened.forEach(simpleScriptStore -> { - pc.withInitSnippet(simpleScriptStore); + pc.withInitSnippet(simpleScriptStore.getStore()); }); if (!bringFlattened.isEmpty()) { pc.withInitSnippet(new ShellInitCommand() { @@ -87,35 +87,27 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore, } return pc; } catch (StackOverflowError t) { - throw new RuntimeException("Unable to set up scripts. Is there a circular script dependency?", t); + throw ErrorEvent.expected( + 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 String initScriptsDirectory(ShellControl proc, List scriptStores) + private static String initScriptsDirectory(ShellControl proc, List> refs) throws Exception { - if (scriptStores.isEmpty()) { + if (refs.isEmpty()) { return null; } - var applicable = scriptStores.stream() + var applicable = refs.stream() .filter(simpleScriptStore -> - simpleScriptStore.getMinimumDialect().isCompatibleTo(proc.getShellDialect())) + simpleScriptStore.getStore().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()) @@ -155,24 +147,25 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore, return targetDir; } - public static List> getDefaultEnabledScripts() { + public static List> getEnabledScripts() { return DataStorage.get().getStoreEntries().stream() - .filter(dataStoreEntry -> dataStoreEntry.getStore() instanceof ScriptStore scriptStore + .filter(dataStoreEntry -> dataStoreEntry.getValidity().isUsable() && + dataStoreEntry.getStore() instanceof ScriptStore scriptStore && scriptStore.getState().isEnabled()) .map(DataStoreEntry::ref) .toList(); } - public static List flatten(List> scripts) { - var seen = new LinkedHashSet(); + public static List> flatten(List> scripts) { + var seen = new LinkedHashSet>(); scripts.forEach(scriptStoreDataStoreEntryRef -> scriptStoreDataStoreEntryRef.getStore().queryFlattenedScripts(seen)); - var dependencies = new HashMap>(); - seen.forEach(simpleScriptStore -> { - var f = new HashSet<>(simpleScriptStore.queryFlattenedScripts()); - f.remove(simpleScriptStore); - dependencies.put(simpleScriptStore, f); + var dependencies = new HashMap, Set>>(); + seen.forEach(ref -> { + var f = new HashSet<>(ref.getStore().queryFlattenedScripts()); + f.remove(ref); + dependencies.put(ref, f); }); var sorted = new ArrayList<>(seen); @@ -208,13 +201,13 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore, // } } - SequencedCollection queryFlattenedScripts() { - var seen = new LinkedHashSet(); + SequencedCollection> queryFlattenedScripts() { + var seen = new LinkedHashSet>(); queryFlattenedScripts(seen); return seen; } - protected abstract void queryFlattenedScripts(LinkedHashSet all); + protected abstract void queryFlattenedScripts(LinkedHashSet> all); public abstract List> getEffectiveScripts(); } 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 1896a7328..d1bb29ffa 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 @@ -10,6 +10,7 @@ import io.xpipe.core.process.ShellInitCommand; import io.xpipe.core.util.ValidationException; import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.ext.base.SelfReferentialStore; import lombok.Getter; import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; @@ -24,20 +25,25 @@ import java.util.stream.Collectors; @Getter @Jacksonized @JsonTypeName("script") -public class SimpleScriptStore extends ScriptStore implements ShellInitCommand.Terminal { +public class SimpleScriptStore extends ScriptStore implements ShellInitCommand.Terminal, SelfReferentialStore { private final ShellDialect minimumDialect; private final String commands; private final boolean initScript; private final boolean shellScript; private final boolean fileScript; + private final boolean runnableScript; public boolean isCompatible(ShellControl shellControl) { var targetType = shellControl.getOriginalShellDialect(); return minimumDialect.isCompatibleTo(targetType); } - public String assemble(ShellControl shellControl) { + public boolean isCompatible(ShellDialect dialect) { + return minimumDialect.isCompatibleTo(dialect); + } + + private String assembleScript(ShellControl shellControl) { if (isCompatible(shellControl)) { var shebang = commands.startsWith("#"); // Fix new lines and shebang @@ -53,35 +59,46 @@ public class SimpleScriptStore extends ScriptStore implements ShellInitCommand.T return null; } + public String assembleScriptChain(ShellControl shellControl) { + var nl = shellControl.getShellDialect().getNewLine().getNewLineString(); + var all = queryFlattenedScripts(); + var r = all.stream().map(ref -> ref.getStore().assembleScript(shellControl)).filter(s -> s != null).toList(); + if (r.isEmpty()) { + return null; + } + return String.join(nl, r); + } + @Override public void checkComplete() throws Throwable { Validators.nonNull(group); super.checkComplete(); Validators.nonNull(minimumDialect); - if (!initScript && !shellScript && !fileScript) { + if (!initScript && !shellScript && !fileScript && !isRunnableScript()) { throw new ValidationException(AppI18n.get("app.valueMustNotBeEmpty")); } } - public void queryFlattenedScripts(LinkedHashSet all) { + public void queryFlattenedScripts(LinkedHashSet> all) { // Prevent loop - all.add(this); + DataStoreEntryRef ref = getSelfEntry().ref(); + all.add(ref); getEffectiveScripts().stream() - .filter(scriptStoreDataStoreEntryRef -> !all.contains(scriptStoreDataStoreEntryRef.getStore())) + .filter(scriptStoreDataStoreEntryRef -> !all.contains(scriptStoreDataStoreEntryRef)) .forEach(scriptStoreDataStoreEntryRef -> { scriptStoreDataStoreEntryRef.getStore().queryFlattenedScripts(all); }); - all.remove(this); - all.add(this); + all.remove(ref); + all.add(ref); } @Override public List> getEffectiveScripts() { - return scripts != null ? scripts.stream().filter(Objects::nonNull).toList() : List.of(); + return scripts != null ? scripts.stream().filter(Objects::nonNull).filter(ref -> ref.get().getValidity().isUsable()).toList() : List.of(); } @Override public Optional terminalContent(ShellControl shellControl) { - return Optional.ofNullable(assemble(shellControl)); + return Optional.ofNullable(assembleScriptChain(shellControl)); } } 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 2a11010a8..54297995a 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 @@ -4,6 +4,7 @@ import io.xpipe.app.comp.base.IntegratedTextAreaComp; import io.xpipe.app.comp.base.ListSelectorComp; import io.xpipe.app.comp.base.SystemStateComp; import io.xpipe.app.comp.store.StoreEntryWrapper; +import io.xpipe.app.comp.store.StoreSection; import io.xpipe.app.comp.store.StoreViewState; import io.xpipe.app.core.AppExtensionManager; import io.xpipe.app.core.AppI18n; @@ -44,11 +45,6 @@ public class SimpleScriptStoreProvider implements EnabledParentStoreProvider, Da return true; } - @Override - public boolean shouldEdit() { - return true; - } - @Override public boolean shouldHaveChildren() { return false; @@ -113,7 +109,7 @@ public class SimpleScriptStoreProvider implements EnabledParentStoreProvider, Da .getDeclaredConstructor(Property.class, boolean.class) .newInstance(dialect, false); - var vals = List.of(0, 1, 2); + var vals = List.of(0, 1, 2, 3); var selectedStart = new ArrayList(); if (st.isInitScript()) { selectedStart.add(0); @@ -124,6 +120,9 @@ public class SimpleScriptStoreProvider implements EnabledParentStoreProvider, Da if (st.isFileScript()) { selectedStart.add(2); } + if (st.isRunnableScript()) { + selectedStart.add(3); + } var name = new Function() { @Override @@ -139,6 +138,10 @@ public class SimpleScriptStoreProvider implements EnabledParentStoreProvider, Da if (integer == 2) { return AppI18n.get("fileScript"); } + + if (integer == 3) { + return AppI18n.get("runnableScript"); + } return "?"; } }; @@ -199,6 +202,7 @@ public class SimpleScriptStoreProvider implements EnabledParentStoreProvider, Da .initScript(selectedExecTypes.contains(0)) .shellScript(selectedExecTypes.contains(1)) .fileScript(selectedExecTypes.contains(2)) + .runnableScript(selectedExecTypes.contains(3)) .build(); }, store) @@ -206,8 +210,9 @@ public class SimpleScriptStoreProvider implements EnabledParentStoreProvider, Da } @Override - public ObservableValue informationString(StoreEntryWrapper wrapper) { - SimpleScriptStore scriptStore = wrapper.getEntry().getStore().asNeeded(); + public ObservableValue informationString(StoreSection section) { + SimpleScriptStore scriptStore = + section.getWrapper().getEntry().getStore().asNeeded(); return new SimpleStringProperty((scriptStore.getMinimumDialect() != null ? scriptStore.getMinimumDialect().getDisplayName() + " " : "") diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceGroupStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceGroupStoreProvider.java index 164927050..5ae46a7ef 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceGroupStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceGroupStoreProvider.java @@ -6,9 +6,11 @@ import io.xpipe.app.comp.store.StoreEntryComp; import io.xpipe.app.comp.store.StoreEntryWrapper; import io.xpipe.app.comp.store.StoreSection; import io.xpipe.app.comp.store.StoreViewState; +import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.DataStoreProvider; import io.xpipe.app.ext.DataStoreUsageCategory; import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.util.ThreadHelper; @@ -16,6 +18,7 @@ import io.xpipe.core.store.DataStore; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; public abstract class AbstractServiceGroupStoreProvider implements DataStoreProvider { @@ -27,7 +30,7 @@ public abstract class AbstractServiceGroupStoreProvider implements DataStoreProv @Override public StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) { var t = createToggleComp(sec); - return StoreEntryComp.create(sec.getWrapper(), t, preferLarge); + return StoreEntryComp.create(sec, t, preferLarge); } private StoreToggleComp createToggleComp(StoreSection sec) { @@ -62,6 +65,26 @@ public abstract class AbstractServiceGroupStoreProvider implements DataStoreProv return t; } + @Override + public ObservableValue informationString(StoreSection section) { + return Bindings.createStringBinding( + () -> { + var all = section.getAllChildren().getList(); + var shown = section.getShownChildren().getList(); + if (shown.size() == 0) { + return null; + } + + var string = all.size() == shown.size() ? all.size() : shown.size() + "/" + all.size(); + return all.size() > 0 + ? (all.size() == 1 ? AppI18n.get("hasService", string) : AppI18n.get("hasServices", string)) + : AppI18n.get("noServices"); + }, + section.getShownChildren().getList(), + section.getAllChildren().getList(), + AppPrefs.get().language()); + } + @Override public Comp stateDisplay(StoreEntryWrapper w) { return new SystemStateComp(new SimpleObjectProperty<>(SystemStateComp.State.SUCCESS)); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStoreProvider.java index eb7053f33..8a542168e 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStoreProvider.java @@ -1,6 +1,7 @@ package io.xpipe.ext.base.service; import io.xpipe.app.comp.base.SystemStateComp; +import io.xpipe.app.comp.store.DenseStoreEntryComp; import io.xpipe.app.comp.store.StoreEntryComp; import io.xpipe.app.comp.store.StoreEntryWrapper; import io.xpipe.app.comp.store.StoreSection; @@ -22,6 +23,11 @@ import java.util.List; public abstract class AbstractServiceStoreProvider implements SingletonSessionStoreProvider, DataStoreProvider { + public String displayName(DataStoreEntry entry) { + AbstractServiceStore s = entry.getStore().asNeeded(); + return DataStorage.get().getStoreEntryDisplayName(s.getHost().get()) + " - Port " + s.getRemotePort(); + } + @Override public DataStoreUsageCategory getUsageCategory() { return DataStoreUsageCategory.TUNNEL; @@ -80,7 +86,7 @@ public abstract class AbstractServiceStoreProvider implements SingletonSessionSt return true; }, sec.getWrapper().getCache())); - return StoreEntryComp.create(sec.getWrapper(), toggle, preferLarge); + return new DenseStoreEntryComp(sec, true, toggle); } @Override @@ -98,8 +104,8 @@ public abstract class AbstractServiceStoreProvider implements SingletonSessionSt } @Override - public ObservableValue informationString(StoreEntryWrapper wrapper) { - AbstractServiceStore s = wrapper.getEntry().getStore().asNeeded(); + public ObservableValue informationString(StoreSection section) { + AbstractServiceStore s = section.getWrapper().getEntry().getStore().asNeeded(); if (s.getLocalPort() != null) { return new SimpleStringProperty("Port " + s.getLocalPort() + " <- " + s.getRemotePort()); } else { diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceGroupStore.java b/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceGroupStore.java index 703987288..fc550a3d0 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceGroupStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceGroupStore.java @@ -22,5 +22,4 @@ public class CustomServiceGroupStore extends AbstractServiceGroupStore informationString(StoreEntryWrapper wrapper) { - FixedServiceStore s = wrapper.getEntry().getStore().asNeeded(); + public ObservableValue informationString(StoreSection section) { + FixedServiceStore s = section.getWrapper().getEntry().getStore().asNeeded(); return new SimpleStringProperty("Port " + s.getRemotePort()); } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/MappedServiceStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/service/MappedServiceStoreProvider.java index 1fb94c6c3..25017f241 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/MappedServiceStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/MappedServiceStoreProvider.java @@ -1,6 +1,8 @@ package io.xpipe.ext.base.service; -import io.xpipe.app.comp.store.StoreEntryWrapper; +import io.xpipe.app.comp.store.StoreSection; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; @@ -9,15 +11,20 @@ import java.util.List; public class MappedServiceStoreProvider extends FixedServiceStoreProvider { + public String displayName(DataStoreEntry entry) { + MappedServiceStore s = entry.getStore().asNeeded(); + return DataStorage.get().getStoreEntryDisplayName(s.getHost().get()) + " - Port " + s.getContainerPort(); + } + @Override public List getPossibleNames() { return List.of("mappedService"); } @Override - public ObservableValue informationString(StoreEntryWrapper wrapper) { - MappedServiceStore s = wrapper.getEntry().getStore().asNeeded(); - return new SimpleStringProperty("Port " + s.getContainerPort() + " -> " + s.getRemotePort()); + public ObservableValue informationString(StoreSection section) { + MappedServiceStore s = section.getWrapper().getEntry().getStore().asNeeded(); + return new SimpleStringProperty("Port " + s.getRemotePort() + " -> " + s.getContainerPort()); } @Override diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceOpenAction.java b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceOpenAction.java index 1863f4776..5fb0aa262 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceOpenAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceOpenAction.java @@ -3,31 +3,30 @@ package io.xpipe.ext.base.service; import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.storage.DataStoreEntryRef; -import io.xpipe.app.util.Hyperlinks; +import io.xpipe.core.store.DataStore; import javafx.beans.value.ObservableValue; -import lombok.Value; +import java.util.List; public class ServiceOpenAction implements ActionProvider { @Override - public LeafDataStoreCallSite getLeafDataStoreCallSite() { - return new LeafDataStoreCallSite() { - + public BranchDataStoreCallSite getBranchDataStoreCallSite() { + return new BranchDataStoreCallSite() { @Override - public boolean isMajor(DataStoreEntryRef o) { + public boolean isMajor(DataStoreEntryRef o) { return true; } @Override - public boolean canLinkTo() { - return true; + public ObservableValue getName(DataStoreEntryRef store) { + return AppI18n.observable("openWebsite"); } @Override - public ActionProvider.Action createAction(DataStoreEntryRef store) { - return new Action(store.getStore()); + public String getIcon(DataStoreEntryRef store) { + return "mdi2s-search-web"; } @Override @@ -36,27 +35,9 @@ public class ServiceOpenAction implements ActionProvider { } @Override - public ObservableValue getName(DataStoreEntryRef store) { - return AppI18n.observable("openWebsite"); - } - - @Override - public String getIcon(DataStoreEntryRef store) { - return "mdi2s-search-web"; + public List getChildren(DataStoreEntryRef store) { + return List.of(new ServiceOpenHttpAction(), new ServiceOpenHttpsAction()); } }; } - - @Value - static class Action implements ActionProvider.Action { - - AbstractServiceStore serviceStore; - - @Override - public void execute() throws Exception { - serviceStore.startSessionIfNeeded(); - var l = serviceStore.getSession().getLocalPort(); - Hyperlinks.open("http://localhost:" + l); - } - } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceOpenHttpAction.java b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceOpenHttpAction.java new file mode 100644 index 000000000..10e8249a7 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceOpenHttpAction.java @@ -0,0 +1,62 @@ +package io.xpipe.ext.base.service; + +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.ext.ActionProvider; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.util.Hyperlinks; + +import javafx.beans.value.ObservableValue; + +import lombok.Value; + +public class ServiceOpenHttpAction implements ActionProvider { + + @Override + public String getId() { + return "serviceOpenHttp"; + } + + @Override + public LeafDataStoreCallSite getLeafDataStoreCallSite() { + return new LeafDataStoreCallSite() { + + @Override + public boolean canLinkTo() { + return true; + } + + @Override + public ActionProvider.Action createAction(DataStoreEntryRef store) { + return new Action(store.getStore()); + } + + @Override + public Class getApplicableClass() { + return AbstractServiceStore.class; + } + + @Override + public ObservableValue getName(DataStoreEntryRef store) { + return AppI18n.observable("openHttp"); + } + + @Override + public String getIcon(DataStoreEntryRef store) { + return "mdi2s-shield-off-outline"; + } + }; + } + + @Value + static class Action implements ActionProvider.Action { + + AbstractServiceStore serviceStore; + + @Override + public void execute() throws Exception { + serviceStore.startSessionIfNeeded(); + var l = serviceStore.getSession().getLocalPort(); + Hyperlinks.open("http://localhost:" + l); + } + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceOpenHttpsAction.java b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceOpenHttpsAction.java new file mode 100644 index 000000000..f727be885 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceOpenHttpsAction.java @@ -0,0 +1,62 @@ +package io.xpipe.ext.base.service; + +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.ext.ActionProvider; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.util.Hyperlinks; + +import javafx.beans.value.ObservableValue; + +import lombok.Value; + +public class ServiceOpenHttpsAction implements ActionProvider { + + @Override + public String getId() { + return "serviceOpenHttps"; + } + + @Override + public LeafDataStoreCallSite getLeafDataStoreCallSite() { + return new LeafDataStoreCallSite() { + + @Override + public boolean canLinkTo() { + return true; + } + + @Override + public ActionProvider.Action createAction(DataStoreEntryRef store) { + return new Action(store.getStore()); + } + + @Override + public Class getApplicableClass() { + return AbstractServiceStore.class; + } + + @Override + public ObservableValue getName(DataStoreEntryRef store) { + return AppI18n.observable("openHttps"); + } + + @Override + public String getIcon(DataStoreEntryRef store) { + return "mdi2s-shield-lock-outline"; + } + }; + } + + @Value + static class Action implements ActionProvider.Action { + + AbstractServiceStore serviceStore; + + @Override + public void execute() throws Exception { + serviceStore.startSessionIfNeeded(); + var l = serviceStore.getSession().getLocalPort(); + Hyperlinks.open("https://localhost:" + l); + } + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/store/ShellStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/store/ShellStoreProvider.java new file mode 100644 index 000000000..424b176a1 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/store/ShellStoreProvider.java @@ -0,0 +1,57 @@ +package io.xpipe.ext.base.store; + +import io.xpipe.app.browser.session.BrowserSessionModel; +import io.xpipe.app.comp.base.OsLogoComp; +import io.xpipe.app.comp.base.SystemStateComp; +import io.xpipe.app.comp.store.StoreEntryWrapper; +import io.xpipe.app.comp.store.StoreSection; +import io.xpipe.app.ext.ActionProvider; +import io.xpipe.app.ext.DataStoreProvider; +import io.xpipe.app.ext.DataStoreUsageCategory; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.util.DataStoreFormatter; +import io.xpipe.app.util.TerminalLauncher; +import io.xpipe.core.store.ShellStore; +import io.xpipe.ext.base.script.ScriptStore; +import javafx.beans.property.BooleanProperty; +import javafx.beans.value.ObservableValue; + +public interface ShellStoreProvider extends DataStoreProvider { + + @Override + default ActionProvider.Action launchAction(DataStoreEntry entry) { + return new ActionProvider.Action() { + @Override + public void execute() throws Exception { + ShellStore store = entry.getStore().asNeeded(); + TerminalLauncher.open(entry, DataStorage.get().getStoreEntryDisplayName(entry), null, ScriptStore.controlWithDefaultScripts(store.control())); + } + }; + } + + @Override + default ActionProvider.Action browserAction(BrowserSessionModel sessionModel, DataStoreEntry store, BooleanProperty busy) { + return new ActionProvider.Action() { + @Override + public void execute() throws Exception { + sessionModel.openFileSystemAsync(store.ref(), null, busy); + } + }; + } + + default Comp stateDisplay(StoreEntryWrapper w) { + return new OsLogoComp(w, SystemStateComp.State.shellState(w)); + } + + @Override + default DataStoreUsageCategory getUsageCategory() { + return DataStoreUsageCategory.SHELL; + } + + @Override + default ObservableValue informationString(StoreSection section) { + return DataStoreFormatter.shellInformation(section.getWrapper()); + } +} diff --git a/ext/base/src/main/java/module-info.java b/ext/base/src/main/java/module-info.java index 91b56b2d1..9a96cbf7b 100644 --- a/ext/base/src/main/java/module-info.java +++ b/ext/base/src/main/java/module-info.java @@ -12,6 +12,9 @@ import io.xpipe.ext.base.script.ScriptDataStorageProvider; import io.xpipe.ext.base.script.ScriptGroupStoreProvider; import io.xpipe.ext.base.script.SimpleScriptStoreProvider; import io.xpipe.ext.base.service.*; +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; @@ -60,10 +63,16 @@ open module io.xpipe.ext.base { JavapAction, JarAction; provides ActionProvider with + StoreStopAction, + StoreStartAction, + StorePauseAction, ServiceOpenAction, + ServiceOpenHttpAction, + ServiceOpenHttpsAction, ServiceCopyUrlAction, CloneStoreAction, RefreshChildrenStoreAction, + RunScriptActionMenu, LaunchStoreAction, XPipeUrlAction, EditStoreAction, diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/apt_update.sh b/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/apt_update.sh deleted file mode 100644 index 3e5dd7717..000000000 --- a/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/apt_update.sh +++ /dev/null @@ -1 +0,0 @@ -sudo apt update \ No newline at end of file diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/apt_upgrade.sh b/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/apt_upgrade.sh new file mode 100644 index 000000000..32ab17aa0 --- /dev/null +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/apt_upgrade.sh @@ -0,0 +1 @@ +sudo apt update && sudo apt upgrade \ No newline at end of file diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/diff.sh b/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/diff.sh new file mode 100644 index 000000000..57a3f23bb --- /dev/null +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/diff.sh @@ -0,0 +1 @@ +diff "$1" "$2" diff --git a/gradle/gradle_scripts/extension.gradle b/gradle/gradle_scripts/extension.gradle index 86dad2305..23e3fb6f3 100644 --- a/gradle/gradle_scripts/extension.gradle +++ b/gradle/gradle_scripts/extension.gradle @@ -66,7 +66,7 @@ configurations { } dependencies { - compileOnly group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.17.1" + compileOnly group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.17.2" compileOnly project(':core') compileOnly project(':beacon') compileOnly project(':app') diff --git a/gradle/gradle_scripts/javafx.gradle b/gradle/gradle_scripts/javafx.gradle index 8720c92d0..243b8d91c 100644 --- a/gradle/gradle_scripts/javafx.gradle +++ b/gradle/gradle_scripts/javafx.gradle @@ -4,10 +4,10 @@ def currentOS = DefaultNativePlatform.currentOperatingSystem; def platform = null if (currentOS.isWindows()) { platform = 'win' -} else if (currentOS.isLinux()) { - platform = 'linux' } else if (currentOS.isMacOsX()) { platform = 'mac' +} else { + platform = 'linux' } if (System.getProperty ("os.arch") == 'aarch64') { diff --git a/gradle/gradle_scripts/junit.gradle b/gradle/gradle_scripts/junit.gradle index 0d96ea7ae..c9583da35 100644 --- a/gradle/gradle_scripts/junit.gradle +++ b/gradle/gradle_scripts/junit.gradle @@ -2,9 +2,9 @@ import org.gradle.api.tasks.testing.logging.TestLogEvent dependencies { testImplementation 'org.hamcrest:hamcrest:2.2' - 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' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.3' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.3' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.3' testRuntimeOnly "org.junit.platform:junit-platform-launcher" } diff --git a/gradle/gradle_scripts/publish-base.gradle b/gradle/gradle_scripts/publish-base.gradle index de1781192..bd1ca77b8 100644 --- a/gradle/gradle_scripts/publish-base.gradle +++ b/gradle/gradle_scripts/publish-base.gradle @@ -3,39 +3,7 @@ java { withSourcesJar() } -def repoUrl = !rootProject.isFullRelease ? 'https://s01.oss.sonatype.org/content/repositories/snapshots/' - : 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/' -def user = project.hasProperty('sonatypeUsername') ? project.property('sonatypeUsername') : System.getenv('SONATYPE_USERNAME') -def pass = project.hasProperty('sonatypePassword') ? project.property('sonatypePassword') : System.getenv('SONATYPE_PASSWORD') - -if (rootProject.isFullRelease) { - publish.finalizedBy(rootProject.getTasks().getByName('closeAndReleaseRepository')) -} - -tasks.withType(GenerateModuleMetadata) { - enabled = false -} - -publishing { - repositories { - maven { - setUrl repoUrl - credentials { - setUsername user - setPassword pass - } - } - } -} - signing { useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) sign publishing.publications.mavenJava } - -nexusStaging { - serverUrl = "https://s01.oss.sonatype.org/service/local/" - packageGroup = "io.xpipe" - username = user - password = pass -} \ No newline at end of file diff --git a/gradle/gradle_scripts/vernacular-1.16.jar b/gradle/gradle_scripts/vernacular-1.16.jar index 261aaa8fb..a95fe369b 100644 Binary files a/gradle/gradle_scripts/vernacular-1.16.jar and b/gradle/gradle_scripts/vernacular-1.16.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6f7a6eb33..dedd5d1e6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/lang/app/strings/fixed_en.properties b/lang/app/strings/fixed_en.properties index f985e93ec..5ec850252 100644 --- a/lang/app/strings/fixed_en.properties +++ b/lang/app/strings/fixed_en.properties @@ -18,6 +18,7 @@ alacrittyMacOs=Alacritty kittyMacOs=Kitty bbedit=BBEdit fleet=Fleet +foot=Foot intellij=IntelliJ IDEA pycharm=PyCharm webstorm=WebStorm diff --git a/lang/app/strings/translations_da.properties b/lang/app/strings/translations_da.properties index d2c3dddbe..e482a3e77 100644 --- a/lang/app/strings/translations_da.properties +++ b/lang/app/strings/translations_da.properties @@ -29,7 +29,7 @@ addService=Service ... addScript=Script ... addHost=Fjernvært ... addShell=Shell-miljø ... -addCommand=Brugerdefineret kommando ... +addCommand=Shell-kommando ... addAutomatically=Søg automatisk ... addOther=Tilføj andet ... addConnection=Tilføj forbindelse @@ -80,7 +80,7 @@ lockCreationAlertHeader=Indstil din nye master-adgangssætning #custom finish=Afslut error=Der opstod en fejl -downloadStageDescription=Downloader filer til din lokale maskine, så du kan trække og slippe dem i dit oprindelige skrivebordsmiljø. +downloadStageDescription=Flytter downloadede filer til dit systems download-bibliotek og åbner det. ok=Ok search=Søg efter newFile=Ny fil @@ -108,7 +108,7 @@ deleteAlertHeader=Vil du slette de ($COUNT$) valgte elementer? selectedElements=Udvalgte elementer: mustNotBeEmpty=$VALUE$ må ikke være tom valueMustNotBeEmpty=Værdien må ikke være tom -transferDescription=Drop filer til overførsel +transferDescription=Drop filer til download dragFiles=Træk filer i browseren dragLocalFiles=Træk lokale filer herfra null=$VALUE$ må ikke være nul @@ -482,3 +482,24 @@ copyId=Kopi ID requireDoubleClickForConnections=Kræver dobbeltklik for forbindelser requireDoubleClickForConnectionsDescription=Hvis den er aktiveret, skal du dobbeltklikke på forbindelser for at starte dem. Det er nyttigt, hvis man er vant til at dobbeltklikke på ting. clearTransferDescription=Ryd valg +selectTab=Vælg fane +closeTab=Luk fanen +closeOtherTabs=Luk andre faner +closeAllTabs=Luk alle faner +closeLeftTabs=Luk faner til venstre +closeRightTabs=Luk faner til højre +addSerial=Seriel ... +connect=Forbind +workspaces=Arbejdsområder +manageWorkspaces=Administrer arbejdsområder +addWorkspace=Tilføj arbejdsområde ... +workspaceAdd=Tilføj et nyt arbejdsområde +workspaceAddDescription=Arbejdsområder er forskellige konfigurationer til at køre XPipe. Hvert arbejdsområde har et datakatalog, hvor alle data gemmes lokalt. Det omfatter forbindelsesdata, indstillinger og meget mere.\n\nHvis du bruger synkroniseringsfunktionen, kan du også vælge at synkronisere hvert arbejdsområde med et forskelligt git-repository. +workspaceName=Navn på arbejdsområde +workspaceNameDescription=Visningsnavnet på arbejdsområdet +workspacePath=Sti til arbejdsområde +workspacePathDescription=Placeringen af arbejdsområdets datakatalog +workspaceCreationAlertTitle=Oprettelse af arbejdsområde +developerForceSshTty=Fremtving SSH TTY +developerForceSshTtyDescription=Få alle SSH-forbindelser til at tildele en pty for at teste understøttelsen af en manglende stderr og en pty. +ttyWarning=Forbindelsen har tvangstildelt en pty/tty og giver ikke en separat stderr-strøm.\n\nDet kan føre til et par problemer.\n\nHvis du kan, så prøv at få forbindelseskommandoen til ikke at tildele en pty. diff --git a/lang/app/strings/translations_de.properties b/lang/app/strings/translations_de.properties index 5ac80370b..8b238fb50 100644 --- a/lang/app/strings/translations_de.properties +++ b/lang/app/strings/translations_de.properties @@ -30,7 +30,7 @@ addScript=Skript ... #custom addHost=Remote Host ... addShell=Shell-Umgebung ... -addCommand=Benutzerdefinierter Befehl ... +addCommand=Shell-Befehl ... addAutomatically=Automatisch suchen ... addOther=Andere hinzufügen ... addConnection=Verbindung hinzufügen @@ -82,7 +82,7 @@ lockCreationAlertHeader=Lege deine neue Master-Passphrase fest #custom finish=Fertigstellen error=Ein Fehler ist aufgetreten -downloadStageDescription=Lädt Dateien auf deinen lokalen Rechner herunter, damit du sie per Drag & Drop in deine native Desktopumgebung ziehen kannst. +downloadStageDescription=Verschiebt heruntergeladene Dateien in das Download-Verzeichnis deines Systems und öffnet sie. ok=Ok search=Suche newFile=Neue Datei @@ -107,7 +107,7 @@ deleteAlertHeader=Willst du die ($COUNT$) ausgewählten Elemente löschen? selectedElements=Ausgewählte Elemente: mustNotBeEmpty=$VALUE$ darf nicht leer sein valueMustNotBeEmpty=Der Wert darf nicht leer sein -transferDescription=Dateien zum Übertragen ablegen +transferDescription=Dateien zum Herunterladen ablegen dragFiles=Dateien im Browser ziehen dragLocalFiles=Lokale Dateien von hier ziehen null=$VALUE$ muss nicht null sein @@ -476,3 +476,24 @@ copyId=ID kopieren requireDoubleClickForConnections=Doppelklick für Verbindungen erforderlich requireDoubleClickForConnectionsDescription=Wenn diese Funktion aktiviert ist, musst du auf die Verbindungen doppelklicken, um sie zu starten. Das ist nützlich, wenn du es gewohnt bist, auf Dinge doppelt zu klicken. clearTransferDescription=Auswahl löschen +selectTab=Registerkarte auswählen +closeTab=Registerkarte schließen +closeOtherTabs=Andere Tabs schließen +closeAllTabs=Alle Registerkarten schließen +closeLeftTabs=Tabs nach links schließen +closeRightTabs=Tabs nach rechts schließen +addSerial=Serielle ... +connect=Verbinden +workspaces=Arbeitsbereiche +manageWorkspaces=Arbeitsbereiche verwalten +addWorkspace=Arbeitsbereich hinzufügen ... +workspaceAdd=Einen neuen Arbeitsbereich hinzufügen +workspaceAddDescription=Arbeitsbereiche sind unterschiedliche Konfigurationen für die Ausführung von XPipe. Jeder Arbeitsbereich hat ein Datenverzeichnis, in dem alle Daten lokal gespeichert werden. Dazu gehören Verbindungsdaten, Einstellungen und mehr.\n\nWenn du die Synchronisierungsfunktion verwendest, kannst du auch wählen, ob du jeden Arbeitsbereich mit einem anderen Git-Repository synchronisieren möchtest. +workspaceName=Name des Arbeitsbereichs +workspaceNameDescription=Der Anzeigename des Arbeitsbereichs +workspacePath=Pfad zum Arbeitsbereich +workspacePathDescription=Der Ort des Datenverzeichnisses des Arbeitsbereichs +workspaceCreationAlertTitle=Arbeitsbereich erstellen +developerForceSshTty=SSH TTY erzwingen +developerForceSshTtyDescription=Lass alle SSH-Verbindungen ein pty zuweisen, um die Unterstützung für einen fehlenden stderr und ein pty zu testen. +ttyWarning=Die Verbindung hat zwangsweise ein pty/tty zugewiesen und stellt keinen separaten stderr-Stream zur Verfügung.\n\nDas kann zu einigen Problemen führen.\n\nWenn du kannst, solltest du dafür sorgen, dass der Verbindungsbefehl kein pty zuweist. diff --git a/lang/app/strings/translations_en.properties b/lang/app/strings/translations_en.properties index e953bb8d9..75bf8f7ff 100644 --- a/lang/app/strings/translations_en.properties +++ b/lang/app/strings/translations_en.properties @@ -29,7 +29,7 @@ addService=Service ... addScript=Script ... addHost=Remote Host ... addShell=Shell Environment ... -addCommand=Custom Command ... +addCommand=Shell Command ... addAutomatically=Search Automatically ... addOther=Add Other ... addConnection=Add Connection @@ -80,7 +80,7 @@ lockCreationAlertHeader=Set your new master passphrase #context: verb, exit finish=Finish error=An error occurred -downloadStageDescription=Downloads files to your local machine, so you can drag and drop them into your native desktop environment. +downloadStageDescription=Moves downloaded files into your system downloads directory and opens it. ok=Ok search=Search newFile=New file @@ -105,7 +105,7 @@ deleteAlertHeader=Do you want to delete the ($COUNT$) selected elements? selectedElements=Selected elements: mustNotBeEmpty=$VALUE$ must not be empty valueMustNotBeEmpty=Value must not be empty -transferDescription=Drop files to transfer +transferDescription=Drop files to download dragFiles=Drag files within browser dragLocalFiles=Drag local files from here null=$VALUE$ must be not null @@ -480,3 +480,24 @@ copyId=Copy ID requireDoubleClickForConnections=Require double click for connections requireDoubleClickForConnectionsDescription=If enabled, you have to double-click connections to launch them. This is useful if you're used to double-clicking things. clearTransferDescription=Clear selection +selectTab=Select tab +closeTab=Close tab +closeOtherTabs=Close other tabs +closeAllTabs=Close all tabs +closeLeftTabs=Close tabs to the left +closeRightTabs=Close tabs to the right +addSerial=Serial ... +connect=Connect +workspaces=Workspaces +manageWorkspaces=Manage workspaces +addWorkspace=Add workspace ... +workspaceAdd=Add a new workspace +workspaceAddDescription=Workspaces are distinct configurations for running XPipe. Every workspace has a data directory where all data is stored locally. This includes connection data, settings, and more.\n\nIf you use the synchronization feature, you can also choose to synchronize each workspace with a different git repository. +workspaceName=Workspace name +workspaceNameDescription=The display name of the workspace +workspacePath=Workspace path +workspacePathDescription=The location of the workspace data directory +workspaceCreationAlertTitle=Workspace creation +developerForceSshTty=Force SSH TTY +developerForceSshTtyDescription=Make all SSH connections allocate a pty to test the support for a missing stderr and a pty. +ttyWarning=The connection has forcefully allocated a pty/tty and does not provide a separate stderr stream.\n\nThis might lead to a few problems.\n\nIf you can, look into making the connection command not allocate a pty. diff --git a/lang/app/strings/translations_es.properties b/lang/app/strings/translations_es.properties index 5ee556745..0f6f65c25 100644 --- a/lang/app/strings/translations_es.properties +++ b/lang/app/strings/translations_es.properties @@ -28,7 +28,7 @@ addService=Servicio ... addScript=Script ... addHost=Host remoto ... addShell=Entorno Shell ... -addCommand=Comando personalizado ... +addCommand=Comandos Shell ... addAutomatically=Buscar automáticamente ... addOther=Añadir otros ... addConnection=Añadir conexión @@ -77,7 +77,7 @@ lockCreationAlertTitle=Establecer frase de contraseña lockCreationAlertHeader=Establece tu nueva frase de contraseña maestra finish=Terminar error=Se ha producido un error -downloadStageDescription=Descarga archivos a tu máquina local, para que puedas arrastrarlos y soltarlos en tu entorno de escritorio nativo. +downloadStageDescription=Mueve los archivos descargados al directorio de descargas de tu sistema y ábrelo. ok=Ok search=Busca en newFile=Nuevo archivo @@ -102,7 +102,7 @@ deleteAlertHeader=¿Quieres borrar los ($COUNT$) elementos seleccionados? selectedElements=Elementos seleccionados: mustNotBeEmpty=$VALUE$ no debe estar vacío valueMustNotBeEmpty=El valor no debe estar vacío -transferDescription=Soltar archivos para transferir +transferDescription=Soltar archivos para descargar dragFiles=Arrastrar archivos dentro del navegador dragLocalFiles=Arrastra archivos locales desde aquí null=$VALUE$ debe ser no nulo @@ -463,3 +463,24 @@ copyId=ID de copia requireDoubleClickForConnections=Requiere doble clic para las conexiones requireDoubleClickForConnectionsDescription=Si está activado, tienes que hacer doble clic en las conexiones para iniciarlas. Esto es útil si estás acostumbrado a hacer doble clic en las cosas. clearTransferDescription=Borrar selección +selectTab=Seleccionar pestaña +closeTab=Cerrar pestaña +closeOtherTabs=Cerrar otras pestañas +closeAllTabs=Cerrar todas las pestañas +closeLeftTabs=Cerrar pestañas a la izquierda +closeRightTabs=Cerrar pestañas a la derecha +addSerial=Serie ... +connect=Conecta +workspaces=Espacios de trabajo +manageWorkspaces=Gestionar espacios de trabajo +addWorkspace=Añadir espacio de trabajo ... +workspaceAdd=Añadir un nuevo espacio de trabajo +workspaceAddDescription=Los espacios de trabajo son configuraciones distintas para ejecutar XPipe. Cada espacio de trabajo tiene un directorio de datos donde se almacenan localmente todos los datos. Esto incluye datos de conexión, configuraciones y más.\n\nSi utilizas la función de sincronización, también puedes elegir sincronizar cada espacio de trabajo con un repositorio git diferente. +workspaceName=Nombre del espacio de trabajo +workspaceNameDescription=El nombre para mostrar del espacio de trabajo +workspacePath=Ruta del espacio de trabajo +workspacePathDescription=La ubicación del directorio de datos del espacio de trabajo +workspaceCreationAlertTitle=Creación de espacios de trabajo +developerForceSshTty=Forzar SSH TTY +developerForceSshTtyDescription=Haz que todas las conexiones SSH asignen una pty para probar la compatibilidad con una stderr y una pty ausentes. +ttyWarning=La conexión ha asignado forzosamente un pty/tty y no proporciona un flujo stderr separado.\n\nEsto puede provocar algunos problemas.\n\nSi puedes, intenta que el comando de conexión no asigne una pty. diff --git a/lang/app/strings/translations_fr.properties b/lang/app/strings/translations_fr.properties index 3bca4cd0c..00696cefe 100644 --- a/lang/app/strings/translations_fr.properties +++ b/lang/app/strings/translations_fr.properties @@ -28,7 +28,7 @@ addService=Service ... addScript=Script ... addHost=Hôte distant ... addShell=Environnement Shell ... -addCommand=Commande personnalisée ... +addCommand=Commande Shell ... addAutomatically=Recherche automatique ... addOther=Ajouter d'autres... addConnection=Ajouter une connexion @@ -77,7 +77,7 @@ lockCreationAlertTitle=Définir une phrase de passe lockCreationAlertHeader=Définis ta nouvelle phrase de passe principale finish=Finir error=Une erreur s'est produite -downloadStageDescription=Télécharge les fichiers sur ta machine locale, afin que tu puisses les faire glisser et les déposer dans ton environnement de bureau natif. +downloadStageDescription=Déplace les fichiers téléchargés dans le répertoire des téléchargements de ton système et l'ouvre. ok=Ok search=Rechercher newFile=Nouveau fichier @@ -102,7 +102,7 @@ deleteAlertHeader=Veux-tu supprimer les ($COUNT$) éléments sélectionnés ? selectedElements=Éléments sélectionnés : mustNotBeEmpty=$VALUE$ ne doit pas être vide valueMustNotBeEmpty=La valeur ne doit pas être vide -transferDescription=Dépose des fichiers à transférer +transferDescription=Dépose des fichiers à télécharger dragFiles=Faire glisser des fichiers dans le navigateur dragLocalFiles=Fais glisser des fichiers locaux à partir d'ici null=$VALUE$ doit être non nul @@ -463,3 +463,24 @@ copyId=ID de copie requireDoubleClickForConnections=Nécessite un double clic pour les connexions requireDoubleClickForConnectionsDescription=Si cette option est activée, tu dois double-cliquer sur les connexions pour les lancer. C'est utile si tu as l'habitude de double-cliquer sur les choses. clearTransferDescription=Effacer la sélection +selectTab=Onglet de sélection +closeTab=Fermer l'onglet +closeOtherTabs=Fermer d'autres onglets +closeAllTabs=Fermer tous les onglets +closeLeftTabs=Ferme les onglets à gauche +closeRightTabs=Ferme les onglets à droite +addSerial=Série ... +connect=Connecter +workspaces=Espaces de travail +manageWorkspaces=Gérer les espaces de travail +addWorkspace=Ajouter un espace de travail ... +workspaceAdd=Ajouter un nouvel espace de travail +workspaceAddDescription=Les espaces de travail sont des configurations distinctes pour l'exécution de XPipe. Chaque espace de travail possède un répertoire de données où toutes les données sont stockées localement. Cela comprend les données de connexion, les paramètres, et plus encore.\n\nSi tu utilises la fonctionnalité de synchronisation, tu peux aussi choisir de synchroniser chaque espace de travail avec un dépôt git différent. +workspaceName=Nom de l'espace de travail +workspaceNameDescription=Le nom d'affichage de l'espace de travail +workspacePath=Chemin d'accès à l'espace de travail +workspacePathDescription=L'emplacement du répertoire de données de l'espace de travail +workspaceCreationAlertTitle=Création d'un espace de travail +developerForceSshTty=Force SSH TTY +developerForceSshTtyDescription=Fais en sorte que toutes les connexions SSH allouent un pty pour tester la prise en charge d'un stderr et d'un pty manquants. +ttyWarning=La connexion a alloué de force un pty/tty et ne fournit pas de flux stderr séparé.\n\nCela peut entraîner quelques problèmes.\n\nSi tu le peux, essaie de faire en sorte que la commande de connexion n'alloue pas de pty. diff --git a/lang/app/strings/translations_it.properties b/lang/app/strings/translations_it.properties index 89665b4f6..b68574585 100644 --- a/lang/app/strings/translations_it.properties +++ b/lang/app/strings/translations_it.properties @@ -28,7 +28,7 @@ addService=Servizio ... addScript=Script ... addHost=Host remoto ... addShell=Ambiente Shell ... -addCommand=Comando personalizzato ... +addCommand=Comando Shell ... addAutomatically=Ricerca automatica ... addOther=Aggiungi altro ... addConnection=Aggiungi connessione @@ -77,7 +77,7 @@ lockCreationAlertTitle=Imposta una passphrase lockCreationAlertHeader=Imposta la tua nuova passphrase principale finish=Terminare error=Si è verificato un errore -downloadStageDescription=Scarica i file sul tuo computer locale, in modo che tu possa trascinarli e rilasciarli nel tuo ambiente desktop nativo. +downloadStageDescription=Sposta i file scaricati nella directory dei download del sistema e li apre. ok=Ok search=Ricerca newFile=Nuovo file @@ -102,7 +102,7 @@ deleteAlertHeader=Vuoi cancellare gli elementi ($COUNT$) selezionati? selectedElements=Elementi selezionati: mustNotBeEmpty=$VALUE$ non deve essere vuoto valueMustNotBeEmpty=Il valore non deve essere vuoto -transferDescription=Rilasciare i file da trasferire +transferDescription=Scaricare i file dragFiles=Trascinare i file nel browser dragLocalFiles=Trascina i file locali da qui null=$VALUE$ deve essere non nullo @@ -463,3 +463,24 @@ copyId=Copia ID requireDoubleClickForConnections=Richiede un doppio clic per le connessioni requireDoubleClickForConnectionsDescription=Se abilitato, devi fare doppio clic sulle connessioni per avviarle. Questo è utile se sei abituato a fare doppio clic. clearTransferDescription=Cancella la selezione +selectTab=Seleziona scheda +closeTab=Chiudi scheda +closeOtherTabs=Chiudere altre schede +closeAllTabs=Chiudi tutte le schede +closeLeftTabs=Chiudere le schede a sinistra +closeRightTabs=Chiudere le schede a destra +addSerial=Seriale ... +connect=Collegare +workspaces=Spazi di lavoro +manageWorkspaces=Gestire gli spazi di lavoro +addWorkspace=Aggiungi spazio di lavoro ... +workspaceAdd=Aggiungere un nuovo spazio di lavoro +workspaceAddDescription=Gli spazi di lavoro sono configurazioni distinte per l'esecuzione di XPipe. Ogni workspace ha una directory di dati in cui vengono memorizzati tutti i dati a livello locale. Questi includono i dati di connessione, le impostazioni e altro ancora.\n\nSe utilizzi la funzione di sincronizzazione, puoi anche scegliere di sincronizzare ogni workspace con un repository git diverso. +workspaceName=Nome dello spazio di lavoro +workspaceNameDescription=Il nome di visualizzazione dell'area di lavoro +workspacePath=Percorso dello spazio di lavoro +workspacePathDescription=La posizione della directory dei dati dell'area di lavoro +workspaceCreationAlertTitle=Creazione di uno spazio di lavoro +developerForceSshTty=Forza SSH TTY +developerForceSshTtyDescription=Fai in modo che tutte le connessioni SSH allocino una pty per testare il supporto di una stderr e di una pty mancanti. +ttyWarning=La connessione ha allocato forzatamente una pty/tty e non fornisce un flusso stderr separato.\n\nQuesto potrebbe causare alcuni problemi.\n\nSe puoi, cerca di fare in modo che il comando di connessione non allarghi una pty. diff --git a/lang/app/strings/translations_ja.properties b/lang/app/strings/translations_ja.properties index ab284f0de..23a32f176 100644 --- a/lang/app/strings/translations_ja.properties +++ b/lang/app/strings/translations_ja.properties @@ -28,7 +28,7 @@ addService=サービス ... addScript=スクリプト ... addHost=リモートホスト ... addShell=シェル環境 ... -addCommand=カスタムコマンド ... +addCommand=シェルコマンド ... addAutomatically=自動的に検索する addOther=その他を追加する addConnection=接続を追加する @@ -77,7 +77,7 @@ lockCreationAlertTitle=パスフレーズを設定する lockCreationAlertHeader=新しいマスターパスフレーズを設定する finish=終了する error=エラーが発生した -downloadStageDescription=ファイルをローカルマシンにダウンロードし、ネイティブのデスクトップ環境にドラッグ&ドロップできるようにする。 +downloadStageDescription=ダウンロードしたファイルをシステムのダウンロード・ディレクトリに移動し、開く。 ok=OK search=検索 newFile=新規ファイル @@ -102,7 +102,7 @@ deleteAlertHeader=選択した ($COUNT$) 要素を削除するか? selectedElements=選択された要素: mustNotBeEmpty=$VALUE$ は空であってはならない valueMustNotBeEmpty=値は空であってはならない -transferDescription=ファイルをドロップして転送する +transferDescription=ファイルをドロップしてダウンロードする dragFiles=ブラウザ内でファイルをドラッグする dragLocalFiles=ここからローカルファイルをドラッグする null=$VALUE$ はnullであってはならない。 @@ -463,3 +463,24 @@ copyId=コピーID requireDoubleClickForConnections=接続にはダブルクリックが必要 requireDoubleClickForConnectionsDescription=有効にすると、接続をダブルクリックしないと起動しなくなる。ダブルクリックに慣れている人には便利な機能だ。 clearTransferDescription=選択範囲をクリアする +selectTab=タブを選択する +closeTab=閉じるタブ +closeOtherTabs=他のタブを閉じる +closeAllTabs=すべてのタブを閉じる +closeLeftTabs=タブを左に閉じる +closeRightTabs=タブを右に閉じる +addSerial=シリアル ... +connect=接続する +workspaces=ワークスペース +manageWorkspaces=ワークスペースを管理する +addWorkspace=ワークスペースを追加する +workspaceAdd=新しいワークスペースを追加する +workspaceAddDescription=ワークスペースは、XPipeを実行するための個別の設定である。すべてのワークスペースには、すべてのデータがローカルに保存されるデータ・ディレクトリがある。これには、接続データや設定などが含まれる。\n\n同期機能を使えば、ワークスペースごとに異なるgitリポジトリと同期させることもできる。 +workspaceName=ワークスペース名 +workspaceNameDescription=ワークスペースの表示名 +workspacePath=ワークスペースのパス +workspacePathDescription=ワークスペースのデータディレクトリの場所 +workspaceCreationAlertTitle=ワークスペースの作成 +developerForceSshTty=強制SSH TTY +developerForceSshTtyDescription=すべてのSSHコネクションにptyを割り当て、stderrとptyがない場合のサポートをテストする。 +ttyWarning=接続が強制的にpty/ttyを割り当て、個別のstderrストリームを提供しない。\n\nこれはいくつかの問題を引き起こす可能性がある。\n\n可能であれば、接続コマンドで pty を割り当てないようにすることを検討してほしい。 diff --git a/lang/app/strings/translations_nl.properties b/lang/app/strings/translations_nl.properties index 75fa9d3d0..956873e88 100644 --- a/lang/app/strings/translations_nl.properties +++ b/lang/app/strings/translations_nl.properties @@ -28,7 +28,7 @@ addService=Service ... addScript=Script ... addHost=Externe host ... addShell=Shell-omgeving ... -addCommand=Aangepaste opdracht ... +addCommand=Shell-commando ... addAutomatically=Automatisch zoeken ... addOther=Andere toevoegen ... addConnection=Verbinding toevoegen @@ -77,7 +77,7 @@ lockCreationAlertTitle=Passphrase instellen lockCreationAlertHeader=Stel je nieuwe hoofdwachtzin in finish=Beëindigen error=Er is een fout opgetreden -downloadStageDescription=Downloadt bestanden naar je lokale computer, zodat je ze naar je eigen desktopomgeving kunt slepen. +downloadStageDescription=Verplaatst gedownloade bestanden naar de downloadmap van je systeem en opent deze. ok=Ok search=Zoeken newFile=Nieuw bestand @@ -102,7 +102,7 @@ deleteAlertHeader=Wil je de ($COUNT$) geselecteerde elementen verwijderen? selectedElements=Geselecteerde elementen: mustNotBeEmpty=$VALUE$ mag niet leeg zijn valueMustNotBeEmpty=Waarde mag niet leeg zijn -transferDescription=Bestanden laten vallen om over te dragen +transferDescription=Bestanden laten vallen om te downloaden dragFiles=Bestanden slepen binnen browser dragLocalFiles=Lokale bestanden van hier slepen null=$VALUE$ moet not null zijn @@ -463,3 +463,24 @@ copyId=ID kopiëren requireDoubleClickForConnections=Dubbelklikken vereist voor verbindingen requireDoubleClickForConnectionsDescription=Als dit is ingeschakeld, moet je dubbelklikken op verbindingen om ze te starten. Dit is handig als je gewend bent om op dingen te dubbelklikken. clearTransferDescription=Selectie wissen +selectTab=Tabblad selecteren +closeTab=Tabblad sluiten +closeOtherTabs=Andere tabbladen sluiten +closeAllTabs=Alle tabbladen sluiten +closeLeftTabs=Tabbladen naar links sluiten +closeRightTabs=Tabbladen naar rechts sluiten +addSerial=Serieel ... +connect=Maak verbinding met +workspaces=Werkruimten +manageWorkspaces=Werkruimten beheren +addWorkspace=Werkruimte toevoegen ... +workspaceAdd=Een nieuwe werkruimte toevoegen +workspaceAddDescription=Workspaces zijn verschillende configuraties voor het uitvoeren van XPipe. Elke workspace heeft een datamap waar alle gegevens lokaal worden opgeslagen. Dit omvat verbindingsgegevens, instellingen en meer.\n\nAls je de synchronisatiefunctie gebruikt, kun je er ook voor kiezen om elke workspace met een andere git repository te synchroniseren. +workspaceName=Naam werkruimte +workspaceNameDescription=De weergavenaam van de werkruimte +workspacePath=Werkruimte pad +workspacePathDescription=De locatie van de gegevensmap van de werkruimte +workspaceCreationAlertTitle=Werkruimte maken +developerForceSshTty=SSH TTY afdwingen +developerForceSshTtyDescription=Laat alle SSH-verbindingen een pty toewijzen om de ondersteuning voor een ontbrekende stderr en een pty te testen. +ttyWarning=De verbinding heeft geforceerd een pty/tty toegewezen en biedt geen aparte stderr stream.\n\nDit kan tot een paar problemen leiden.\n\nAls je kunt, kijk dan of je het connection commando geen pty kunt laten toewijzen. diff --git a/lang/app/strings/translations_pt.properties b/lang/app/strings/translations_pt.properties index b1e3bdb00..911b410bb 100644 --- a/lang/app/strings/translations_pt.properties +++ b/lang/app/strings/translations_pt.properties @@ -28,7 +28,7 @@ addService=Serviço ... addScript=Script ... addHost=Anfitrião remoto ... addShell=Ambiente Shell ... -addCommand=Comando personalizado ... +addCommand=Comando Shell ... addAutomatically=Pesquisa automaticamente ... addOther=Adiciona outro ... addConnection=Adicionar ligação @@ -77,7 +77,7 @@ lockCreationAlertTitle=Define a frase-chave lockCreationAlertHeader=Define a tua nova frase-chave principal finish=Termina error=Ocorreu um erro -downloadStageDescription=Descarrega ficheiros para a sua máquina local, para que possa arrastá-los e largá-los no seu ambiente de trabalho nativo. +downloadStageDescription=Move os ficheiros transferidos para o diretório de transferências do sistema e abre-o. ok=Ok search=Procura newFile=Novo ficheiro @@ -463,3 +463,24 @@ copyId=ID de cópia requireDoubleClickForConnections=Exige duplo clique para ligações requireDoubleClickForConnectionsDescription=Se estiver ativado, tens de fazer duplo clique nas ligações para as iniciar. Isto é útil se estiveres habituado a fazer duplo clique em coisas. clearTransferDescription=Limpar seleção +selectTab=Selecionar separador +closeTab=Fecha o separador +closeOtherTabs=Fecha outros separadores +closeAllTabs=Fecha todos os separadores +closeLeftTabs=Fecha os separadores à esquerda +closeRightTabs=Fecha os separadores à direita +addSerial=Série ... +connect=Liga-te +workspaces=Espaços de trabalho +manageWorkspaces=Gere espaços de trabalho +addWorkspace=Adiciona um espaço de trabalho ... +workspaceAdd=Adiciona um novo espaço de trabalho +workspaceAddDescription=Os espaços de trabalho são configurações distintas para executar o XPipe. Cada espaço de trabalho tem um diretório de dados onde todos os dados são armazenados localmente. Isto inclui dados de ligação, definições e muito mais.\n\nSe utilizares a funcionalidade de sincronização, também podes optar por sincronizar cada espaço de trabalho com um repositório git diferente. +workspaceName=Nome do espaço de trabalho +workspaceNameDescription=O nome de apresentação do espaço de trabalho +workspacePath=Caminho do espaço de trabalho +workspacePathDescription=A localização do diretório de dados do espaço de trabalho +workspaceCreationAlertTitle=Criação de espaço de trabalho +developerForceSshTty=Força o SSH TTY +developerForceSshTtyDescription=Faz com que todas as ligações SSH atribuam um pty para testar o suporte para um stderr e um pty em falta. +ttyWarning=A ligação atribuiu à força um pty/tty e não fornece um fluxo stderr separado.\n\nIsto pode levar a alguns problemas.\n\nSe puderes, tenta fazer com que o comando de ligação não atribua um pty. diff --git a/lang/app/strings/translations_ru.properties b/lang/app/strings/translations_ru.properties index bb396d21d..762edf59b 100644 --- a/lang/app/strings/translations_ru.properties +++ b/lang/app/strings/translations_ru.properties @@ -28,7 +28,7 @@ addService=Сервис ... addScript=Скрипт ... addHost=Удаленный хост ... addShell=Shell Environment ... -addCommand=Пользовательская команда ... +addCommand=Shell Command ... addAutomatically=Поиск в автоматическом режиме ... addOther=Add Other ... addConnection=Добавить соединение @@ -77,7 +77,7 @@ lockCreationAlertTitle=Установите парольную фразу lockCreationAlertHeader=Установите новую главную кодовую фразу finish=Закончи error=Произошла ошибка -downloadStageDescription=Загрузи файлы на локальную машину, чтобы ты мог перетащить их в родное окружение рабочего стола. +downloadStageDescription=Перемести скачанные файлы в системный каталог загрузок и открой его. ok=Ок search=Поиск newFile=Новый файл @@ -102,7 +102,7 @@ deleteAlertHeader=Хочешь удалить ($COUNT$) выбранные эл selectedElements=Выбранные элементы: mustNotBeEmpty=$VALUE$ не должен быть пустым valueMustNotBeEmpty=Значение не должно быть пустым -transferDescription=Сбрасывать файлы для передачи +transferDescription=Сбрасывать файлы для загрузки dragFiles=Перетаскивание файлов в браузере dragLocalFiles=Перетащите локальные файлы отсюда null=$VALUE$ должен быть не нулевым @@ -463,3 +463,24 @@ copyId=Идентификатор копии requireDoubleClickForConnections=Требуется двойной щелчок для подключения requireDoubleClickForConnectionsDescription=Если эта опция включена, тебе придется дважды щелкнуть по соединениям, чтобы запустить их. Это полезно, если ты привык все запускать двойным щелчком. clearTransferDescription=Четкий выбор +selectTab=Выберите вкладку +closeTab=Закройте вкладку +closeOtherTabs=Закрыть другие вкладки +closeAllTabs=Закрыть все вкладки +closeLeftTabs=Закрыть вкладки слева +closeRightTabs=Закрывать вкладки справа +addSerial=Серийный ... +connect=Connect +workspaces=Рабочие пространства +manageWorkspaces=Управляй рабочими пространствами +addWorkspace=Добавь рабочее пространство ... +workspaceAdd=Добавьте новое рабочее пространство +workspaceAddDescription=Рабочие пространства - это отдельные конфигурации для запуска XPipe. В каждом рабочем пространстве есть каталог данных, где все данные хранятся локально. Сюда входят данные о соединениях, настройки и многое другое.\n\nЕсли ты используешь функцию синхронизации, ты также можешь выбрать синхронизацию каждого рабочего пространства с отдельным git-репозиторием. +workspaceName=Имя рабочей области +workspaceNameDescription=Отображаемое имя рабочей области +workspacePath=Путь к рабочему пространству +workspacePathDescription=Расположение каталога данных рабочей области +workspaceCreationAlertTitle=Создание рабочего пространства +developerForceSshTty=Принудительный SSH TTY +developerForceSshTtyDescription=Заставь все SSH-соединения выделять pty, чтобы проверить поддержку отсутствующего stderr и pty. +ttyWarning=Соединение принудительно выделило pty/tty и не предоставляет отдельный поток stderr.\n\nЭто может привести к нескольким проблемам.\n\nЕсли можешь, попробуй сделать так, чтобы команда подключения не выделяла pty. diff --git a/lang/app/strings/translations_tr.properties b/lang/app/strings/translations_tr.properties index 8f26bf3ea..b4ff5482d 100644 --- a/lang/app/strings/translations_tr.properties +++ b/lang/app/strings/translations_tr.properties @@ -28,7 +28,7 @@ addService=Hizmet ... addScript=Senaryo ... addHost=Uzak Ana Bilgisayar ... addShell=Shell Çevre ... -addCommand=Özel Komut ... +addCommand=Kabuk Komutu ... addAutomatically=Otomatik Olarak Ara ... addOther=Diğerlerini Ekle ... addConnection=Bağlantı Ekle @@ -77,7 +77,7 @@ lockCreationAlertTitle=Parolayı ayarla lockCreationAlertHeader=Yeni ana parolanızı ayarlayın finish=Bitirmek error=Bir hata oluştu -downloadStageDescription=Dosyaları yerel makinenize indirir, böylece bunları yerel masaüstü ortamınıza sürükleyip bırakabilirsiniz. +downloadStageDescription=İndirilen dosyaları sisteminizin indirilenler dizinine taşır ve açar. ok=Tamam search=Arama newFile=Yeni dosya @@ -102,7 +102,7 @@ deleteAlertHeader=($COUNT$) seçili öğeleri silmek istiyor musunuz? selectedElements=Seçilen unsurlar: mustNotBeEmpty=$VALUE$ boş olmamalıdır valueMustNotBeEmpty=Değer boş olmamalıdır -transferDescription=Aktarılacak dosyaları bırakın +transferDescription=İndirilecek dosyaları bırakın dragFiles=Dosyaları tarayıcı içinde sürükleyin dragLocalFiles=Yerel dosyaları buradan sürükleyin null=$VALUE$ null olmamalıdır @@ -464,3 +464,24 @@ copyId=Kopya Kimliği requireDoubleClickForConnections=Bağlantılar için çift tıklama gerektir requireDoubleClickForConnectionsDescription=Etkinleştirilirse, bağlantıları başlatmak için çift tıklamanız gerekir. Bu, bir şeyleri çift tıklamaya alışkınsanız kullanışlıdır. clearTransferDescription=Seçimi temizle +selectTab=Sekme seçin +closeTab=Sekmeyi kapat +closeOtherTabs=Diğer sekmeleri kapatın +closeAllTabs=Tüm sekmeleri kapat +closeLeftTabs=Sekmeleri sola doğru kapatın +closeRightTabs=Sekmeleri sağa doğru kapatın +addSerial=Seri ... +connect=Bağlan +workspaces=Çalışma Alanları +manageWorkspaces=Çalışma alanlarını yönetme +addWorkspace=Çalışma alanı ekle ... +workspaceAdd=Yeni bir çalışma alanı ekleme +workspaceAddDescription=Çalışma alanları XPipe'ı çalıştırmak için farklı konfigürasyonlardır. Her çalışma alanı, tüm verilerin yerel olarak depolandığı bir veri dizinine sahiptir. Buna bağlantı verileri, ayarlar ve daha fazlası dahildir.\n\nSenkronizasyon özelliğini kullanırsanız, her çalışma alanını farklı bir git deposu ile senkronize etmeyi de seçebilirsiniz. +workspaceName=Çalışma alanı adı +workspaceNameDescription=Çalışma alanının görünen adı +workspacePath=Çalışma alanı yolu +workspacePathDescription=Çalışma alanı veri dizininin konumu +workspaceCreationAlertTitle=Çalışma alanı oluşturma +developerForceSshTty=SSH TTY'yi Zorla +developerForceSshTtyDescription=Eksik bir stderr ve bir pty desteğini test etmek için tüm SSH bağlantılarının bir pty ayırmasını sağlayın. +ttyWarning=Bağlantı zorla bir pty/tty ayırmış ve ayrı bir stderr akışı sağlamıyor.\n\nBu durum birkaç soruna yol açabilir.\n\nEğer yapabiliyorsanız, bağlantı komutunun bir pty tahsis etmemesini sağlayın. diff --git a/lang/app/strings/translations_zh.properties b/lang/app/strings/translations_zh.properties index 3a797a7f6..ae866d435 100644 --- a/lang/app/strings/translations_zh.properties +++ b/lang/app/strings/translations_zh.properties @@ -28,7 +28,7 @@ addService=服务 ... addScript=脚本 ... addHost=远程主机 ... addShell=外壳环境 ... -addCommand=自定义命令 ... +addCommand=Shell 命令 ... addAutomatically=自动搜索 ... addOther=添加其他 ... addConnection=添加连接 @@ -77,7 +77,7 @@ lockCreationAlertTitle=设置口令 lockCreationAlertHeader=设置新的主密码 finish=完成 error=发生错误 -downloadStageDescription=将文件下载到本地计算机,以便拖放到本地桌面环境中。 +downloadStageDescription=将下载的文件移动到系统下载目录并打开。 ok=好的 search=搜索 newFile=新文件 @@ -102,7 +102,7 @@ deleteAlertHeader=您想删除 ($COUNT$) 选定的元素吗? selectedElements=选定要素: mustNotBeEmpty=$VALUE$ 不得为空 valueMustNotBeEmpty=值不得为空 -transferDescription=下拉传输文件 +transferDescription=下载文件 dragFiles=在浏览器中拖动文件 dragLocalFiles=从此处拖动本地文件 null=$VALUE$ 必须为非空 @@ -463,3 +463,24 @@ copyId=复制 ID requireDoubleClickForConnections=要求双击连接 requireDoubleClickForConnectionsDescription=如果启用,则必须双击连接才能启动。如果您习惯双击事物,这将非常有用。 clearTransferDescription=清除选择 +selectTab=选择选项卡 +closeTab=关闭选项卡 +closeOtherTabs=关闭其他标签页 +closeAllTabs=关闭所有标签页 +closeLeftTabs=向左关闭标签 +closeRightTabs=向右关闭标签页 +addSerial=串行 ... +connect=连接 +workspaces=工作空间 +manageWorkspaces=管理工作区 +addWorkspace=添加工作区 ... +workspaceAdd=添加新工作区 +workspaceAddDescription=工作区是运行 XPipe 的独特配置。每个工作区都有一个数据目录,本地存储所有数据。其中包括连接数据、设置等。\n\n如果使用同步功能,您还可以选择将每个工作区与不同的 git 仓库同步。 +workspaceName=工作区名称 +workspaceNameDescription=工作区的显示名称 +workspacePath=工作区路径 +workspacePathDescription=工作区数据目录的位置 +workspaceCreationAlertTitle=创建工作区 +developerForceSshTty=强制 SSH TTY +developerForceSshTtyDescription=让所有 SSH 连接都分配一个 pty,以测试对缺失的 stderr 和 pty 的支持。 +ttyWarning=连接强行分配了 pty/tty,且未提供单独的 stderr 流。\n\n这可能会导致一些问题。\n\n如果可以,请考虑让连接命令不分配 pty。 diff --git a/lang/base/strings/translations_da.properties b/lang/base/strings/translations_da.properties index 57d850e2b..05bdd614a 100644 --- a/lang/base/strings/translations_da.properties +++ b/lang/base/strings/translations_da.properties @@ -154,9 +154,10 @@ serviceHostDescription=Den vært, som tjenesten kører på openWebsite=Åben hjemmeside customServiceGroup.displayName=Service-gruppe customServiceGroup.displayDescription=Gruppér flere tjenester i én kategori -initScript=Kører på shell init -shellScript=Gør script tilgængeligt under shell-session -fileScript=Gør det muligt at kalde et script med filargumenter i filbrowseren +initScript=Init-script - køres ved shell-init +shellScript=Shell-sessionsscript - Gør et script tilgængeligt til at køre under en shell-session +runnableScript=Kørbart script - Tillad, at scriptet køres direkte fra connection hub'en +fileScript=Filscript - Gør det muligt at kalde et script med filargumenter i filbrowseren runScript=Kør script copyUrl=Kopier URL fixedServiceGroup.displayName=Service-gruppe @@ -164,6 +165,12 @@ fixedServiceGroup.displayDescription=Liste over tilgængelige tjenester på et s mappedService.displayName=Service mappedService.displayDescription=Interagere med en tjeneste, der er eksponeret af en container customService.displayName=Service -customService.displayDescription=Tilføj en brugerdefineret tjeneste til tunnel og åben +customService.displayDescription=Tilføj en ekstern serviceport til tunnel til din lokale maskine fixedService.displayName=Service fixedService.displayDescription=Brug en foruddefineret tjeneste +noServices=Ingen tilgængelige tjenester +hasServices=$COUNT$ tilgængelige tjenester +hasService=$COUNT$ tilgængelig tjeneste +openHttp=Åben HTTP-tjeneste +openHttps=Åben HTTPS-tjeneste +noScriptsAvailable=Ingen tilgængelige scripts diff --git a/lang/base/strings/translations_de.properties b/lang/base/strings/translations_de.properties index 55f04ac61..889e00cd0 100644 --- a/lang/base/strings/translations_de.properties +++ b/lang/base/strings/translations_de.properties @@ -145,9 +145,10 @@ serviceHostDescription=Der Host, auf dem der Dienst läuft openWebsite=Website öffnen customServiceGroup.displayName=Dienstgruppe customServiceGroup.displayDescription=Mehrere Dienste in einer Kategorie zusammenfassen -initScript=Auf der Shell init ausführen -shellScript=Skript während der Shell-Sitzung verfügbar machen -fileScript=Skriptaufruf mit Dateiargumenten im Dateibrowser zulassen +initScript=Init-Skript - Wird beim Shell-Init ausgeführt +shellScript=Shell-Sitzungsskript - Skript für die Ausführung während einer Shell-Sitzung verfügbar machen +runnableScript=Ausführbares Skript - Erlaubt die direkte Ausführung eines Skripts über den Verbindungs-Hub +fileScript=Dateiskript - Erlaubt den Aufruf eines Skripts mit Dateiargumenten im Dateibrowser runScript=Skript ausführen copyUrl=URL kopieren fixedServiceGroup.displayName=Dienstgruppe @@ -155,6 +156,12 @@ fixedServiceGroup.displayDescription=Liste der verfügbaren Dienste auf einem Sy mappedService.displayName=Dienst mappedService.displayDescription=Interaktion mit einem Dienst, der von einem Container angeboten wird customService.displayName=Dienst -customService.displayDescription=Einen benutzerdefinierten Dienst zum Tunnel hinzufügen und öffnen +customService.displayDescription=Füge einen Remote Service Port hinzu, um einen Tunnel zu deinem lokalen Rechner zu erstellen fixedService.displayName=Dienst fixedService.displayDescription=Einen vordefinierten Dienst verwenden +noServices=Keine verfügbaren Dienste +hasServices=$COUNT$ verfügbare Dienste +hasService=$COUNT$ verfügbarer Dienst +openHttp=Offener HTTP-Dienst +openHttps=HTTPS-Dienst öffnen +noScriptsAvailable=Keine Skripte verfügbar diff --git a/lang/base/strings/translations_en.properties b/lang/base/strings/translations_en.properties index 08f77e1d2..16315d281 100644 --- a/lang/base/strings/translations_en.properties +++ b/lang/base/strings/translations_en.properties @@ -143,9 +143,10 @@ serviceHostDescription=The host the service is running on openWebsite=Open website customServiceGroup.displayName=Service group customServiceGroup.displayDescription=Group multiple services into one category -initScript=Run on shell init -shellScript=Make script available during shell session -fileScript=Allow script to be called with file arguments in the file browser +initScript=Init script - Run on shell init +shellScript=Shell session script - Make script available to run during a shell session +runnableScript=Runnable script - Allow script to be run directly from the connection hub +fileScript=File script - Allow script to be called with file arguments in the file browser runScript=Run script copyUrl=Copy URL fixedServiceGroup.displayName=Service group @@ -153,8 +154,14 @@ fixedServiceGroup.displayDescription=List the available services on a system mappedService.displayName=Service mappedService.displayDescription=Interact with a service exposed by a container customService.displayName=Service -customService.displayDescription=Add a custom service to tunnel and open +customService.displayDescription=Add a remote service port to tunnel to your local machine fixedService.displayName=Service fixedService.displayDescription=Use a predefined service +noServices=No available services +hasServices=$COUNT$ available services +hasService=$COUNT$ available service +openHttp=Open HTTP service +openHttps=Open HTTPS service +noScriptsAvailable=No scripts available diff --git a/lang/base/strings/translations_es.properties b/lang/base/strings/translations_es.properties index bbcd05db6..d3cdd6a24 100644 --- a/lang/base/strings/translations_es.properties +++ b/lang/base/strings/translations_es.properties @@ -143,9 +143,10 @@ serviceHostDescription=El host en el que se ejecuta el servicio openWebsite=Abrir sitio web customServiceGroup.displayName=Grupo de servicios customServiceGroup.displayDescription=Agrupa varios servicios en una categoría -initScript=Ejecutar en shell init -shellScript=Hacer que el script esté disponible durante la sesión shell -fileScript=Permitir llamar a un script con argumentos de archivo en el explorador de archivos +initScript=Script de init - Se ejecuta en el shell init +shellScript=Script de sesión de shell - Hacer que un script esté disponible para ejecutarse durante una sesión de shell +runnableScript=Script ejecutable - Permite que el script se ejecute directamente desde el concentrador de conexiones +fileScript=Script de archivo - Permite llamar al script con argumentos de archivo en el explorador de archivos runScript=Ejecutar script copyUrl=Copiar URL fixedServiceGroup.displayName=Grupo de servicios @@ -153,6 +154,12 @@ fixedServiceGroup.displayDescription=Enumerar los servicios disponibles en un si mappedService.displayName=Servicio mappedService.displayDescription=Interactúa con un servicio expuesto por un contenedor customService.displayName=Servicio -customService.displayDescription=Añade un servicio personalizado para tunelizar y abrir +customService.displayDescription=Añade un puerto de servicio remoto para hacer un túnel a tu máquina local fixedService.displayName=Servicio fixedService.displayDescription=Utilizar un servicio predefinido +noServices=No hay servicios disponibles +hasServices=$COUNT$ servicios disponibles +hasService=$COUNT$ servicio disponible +openHttp=Servicio HTTP abierto +openHttps=Abrir servicio HTTPS +noScriptsAvailable=No hay guiones disponibles diff --git a/lang/base/strings/translations_fr.properties b/lang/base/strings/translations_fr.properties index 476f18079..df537b870 100644 --- a/lang/base/strings/translations_fr.properties +++ b/lang/base/strings/translations_fr.properties @@ -143,9 +143,10 @@ serviceHostDescription=L'hôte sur lequel le service est exécuté openWebsite=Ouvrir un site web customServiceGroup.displayName=Groupe de service customServiceGroup.displayDescription=Regrouper plusieurs services dans une même catégorie -initScript=Exécute sur le shell init -shellScript=Rendre le script disponible pendant la session shell -fileScript=Permet d'appeler un script avec des arguments de fichier dans le navigateur de fichiers +initScript=Script d'initialisation - Exécuté lors de l'initialisation de l'interpréteur de commandes +shellScript=Script de session shell - Rendre un script disponible pour être exécuté au cours d'une session shell +runnableScript=Script exécutable - Permet d'exécuter un script directement à partir du concentrateur de connexion +fileScript=Script de fichier - Permet d'appeler un script avec des arguments de fichier dans le navigateur de fichiers runScript=Exécuter un script copyUrl=Copier l'URL fixedServiceGroup.displayName=Groupe de service @@ -153,6 +154,12 @@ fixedServiceGroup.displayDescription=Liste les services disponibles sur un syst mappedService.displayName=Service mappedService.displayDescription=Interagir avec un service exposé par un conteneur customService.displayName=Service -customService.displayDescription=Ajouter un service personnalisé au tunnel et à l'ouverture +customService.displayDescription=Ajoute un port de service à distance pour établir un tunnel vers ta machine locale fixedService.displayName=Service fixedService.displayDescription=Utiliser un service prédéfini +noServices=Aucun service disponible +hasServices=$COUNT$ services disponibles +hasService=$COUNT$ service disponible +openHttp=Service HTTP ouvert +openHttps=Service HTTPS ouvert +noScriptsAvailable=Pas de scripts disponibles diff --git a/lang/base/strings/translations_it.properties b/lang/base/strings/translations_it.properties index 11cdd26e5..955cd4c50 100644 --- a/lang/base/strings/translations_it.properties +++ b/lang/base/strings/translations_it.properties @@ -143,9 +143,10 @@ serviceHostDescription=L'host su cui è in esecuzione il servizio openWebsite=Sito web aperto customServiceGroup.displayName=Gruppo di servizio customServiceGroup.displayDescription=Raggruppa più servizi in un'unica categoria -initScript=Eseguire su shell init -shellScript=Rendere disponibile lo script durante la sessione di shell -fileScript=Consente di richiamare uno script con argomenti di file nel browser di file +initScript=Script di avvio - Eseguito all'avvio della shell +shellScript=Script di sessione di shell - Rendere disponibile uno script da eseguire durante una sessione di shell +runnableScript=Script eseguibile - Consente l'esecuzione di uno script direttamente dall'hub di connessione +fileScript=File script - Consente di richiamare uno script con argomenti di file nel browser di file runScript=Esegui script copyUrl=Copia URL fixedServiceGroup.displayName=Gruppo di servizio @@ -153,6 +154,12 @@ fixedServiceGroup.displayDescription=Elenco dei servizi disponibili su un sistem mappedService.displayName=Servizio mappedService.displayDescription=Interagire con un servizio esposto da un contenitore customService.displayName=Servizio -customService.displayDescription=Aggiungi un servizio personalizzato per il tunnel e l'apertura +customService.displayDescription=Aggiungi una porta di servizio remoto per creare un tunnel verso la tua macchina locale fixedService.displayName=Servizio fixedService.displayDescription=Utilizzare un servizio predefinito +noServices=Nessun servizio disponibile +hasServices=$COUNT$ servizi disponibili +hasService=$COUNT$ servizio disponibile +openHttp=Servizio HTTP aperto +openHttps=Servizio HTTPS aperto +noScriptsAvailable=Non sono disponibili script diff --git a/lang/base/strings/translations_ja.properties b/lang/base/strings/translations_ja.properties index 0dfaae5d6..1157b5a10 100644 --- a/lang/base/strings/translations_ja.properties +++ b/lang/base/strings/translations_ja.properties @@ -143,9 +143,10 @@ serviceHostDescription=サービスが稼働しているホスト openWebsite=オープンウェブサイト customServiceGroup.displayName=サービスグループ customServiceGroup.displayDescription=複数のサービスを1つのカテゴリーにまとめる -initScript=シェル init で実行する -shellScript=シェルセッション中にスクリプトを利用可能にする -fileScript=ファイルブラウザでファイル引数を指定してスクリプトを呼び出せるようにする +initScript=initスクリプト - シェルのinit時に実行する +shellScript=シェルセッションスクリプト - シェルセッション中に実行可能なスクリプトを作成する。 +runnableScript=実行可能なスクリプト - 接続ハブからスクリプトを直接実行できるようにする +fileScript=ファイルスクリプト - ファイルブラウザでファイル引数を指定してスクリプトを呼び出せるようにする runScript=スクリプトを実行する copyUrl=URLをコピーする fixedServiceGroup.displayName=サービスグループ @@ -153,6 +154,12 @@ fixedServiceGroup.displayDescription=システムで利用可能なサービス mappedService.displayName=サービス mappedService.displayDescription=コンテナによって公開されたサービスとやりとりする customService.displayName=サービス -customService.displayDescription=トンネルとオープンにカスタムサービスを追加する +customService.displayDescription=リモートサービスのポートを追加してローカルマシンにトンネリングする fixedService.displayName=サービス fixedService.displayDescription=定義済みのサービスを使う +noServices=利用可能なサービスはない +hasServices=$COUNT$ 利用可能なサービス +hasService=$COUNT$ 利用可能なサービス +openHttp=オープンHTTPサービス +openHttps=HTTPSサービスを開く +noScriptsAvailable=スクリプトはない diff --git a/lang/base/strings/translations_nl.properties b/lang/base/strings/translations_nl.properties index ea34666bc..4fa31e6e6 100644 --- a/lang/base/strings/translations_nl.properties +++ b/lang/base/strings/translations_nl.properties @@ -143,9 +143,10 @@ serviceHostDescription=De host waarop de service draait openWebsite=Open website customServiceGroup.displayName=Servicegroep customServiceGroup.displayDescription=Groepeer meerdere diensten in één categorie -initScript=Uitvoeren op shell init -shellScript=Script beschikbaar maken tijdens shellsessie -fileScript=Laat toe dat een script wordt aangeroepen met bestandsargumenten in de bestandsbrowser +initScript=Init script - Uitvoeren op shell init +shellScript=Shell-sessiescript - Script beschikbaar maken om uit te voeren tijdens een shellsessie +runnableScript=Runnable script - Maakt het mogelijk om een script direct vanuit de verbindingshub uit te voeren +fileScript=Bestandsscript - Laat toe dat script wordt aangeroepen met bestandsargumenten in de bestandsbrowser runScript=Script uitvoeren copyUrl=URL kopiëren fixedServiceGroup.displayName=Servicegroep @@ -153,6 +154,12 @@ fixedServiceGroup.displayDescription=Een lijst van beschikbare services op een s mappedService.displayName=Service mappedService.displayDescription=Interactie met een service die wordt aangeboden door een container customService.displayName=Service -customService.displayDescription=Een aangepaste service toevoegen aan tunnel en openen +customService.displayDescription=Een servicepoort op afstand toevoegen om te tunnelen naar je lokale machine fixedService.displayName=Service fixedService.displayDescription=Een vooraf gedefinieerde service gebruiken +noServices=Geen beschikbare diensten +hasServices=$COUNT$ beschikbare diensten +hasService=$COUNT$ beschikbare dienst +openHttp=Open HTTP service +openHttps=Open HTTPS service +noScriptsAvailable=Geen scripts beschikbaar diff --git a/lang/base/strings/translations_pt.properties b/lang/base/strings/translations_pt.properties index 58740c2d5..83c86efa3 100644 --- a/lang/base/strings/translations_pt.properties +++ b/lang/base/strings/translations_pt.properties @@ -143,9 +143,10 @@ serviceHostDescription=O anfitrião em que o serviço está a ser executado openWebsite=Abre o sítio Web customServiceGroup.displayName=Grupo de serviços customServiceGroup.displayDescription=Agrupa vários serviços numa categoria -initScript=Corre no shell init -shellScript=Torna o script disponível durante a sessão da shell -fileScript=Permite que o script seja chamado com argumentos de ficheiro no navegador de ficheiros +initScript=Script de inicialização - Executa na inicialização do shell +shellScript=Script de sessão de shell - Torna o script disponível para ser executado durante uma sessão de shell +runnableScript=Script executável - Permite que o script seja executado diretamente a partir do hub de ligação +fileScript=Script de ficheiro - Permite que o script seja chamado com argumentos de ficheiro no navegador de ficheiros runScript=Executa o script copyUrl=Copia o URL fixedServiceGroup.displayName=Grupo de serviços @@ -153,6 +154,12 @@ fixedServiceGroup.displayDescription=Lista os serviços disponíveis num sistema mappedService.displayName=Serviço mappedService.displayDescription=Interage com um serviço exposto por um contentor customService.displayName=Serviço -customService.displayDescription=Adiciona um serviço personalizado ao túnel e abre +customService.displayDescription=Adiciona uma porta de serviço remoto para criar um túnel para a tua máquina local fixedService.displayName=Serviço fixedService.displayDescription=Utiliza um serviço predefinido +noServices=Não há serviços disponíveis +hasServices=$COUNT$ serviços disponíveis +hasService=$COUNT$ serviço disponível +openHttp=Abre o serviço HTTP +openHttps=Abre o serviço HTTPS +noScriptsAvailable=Não há scripts disponíveis diff --git a/lang/base/strings/translations_ru.properties b/lang/base/strings/translations_ru.properties index a9ecfcb5a..0c76ed775 100644 --- a/lang/base/strings/translations_ru.properties +++ b/lang/base/strings/translations_ru.properties @@ -143,9 +143,10 @@ serviceHostDescription=Хост, на котором запущена служб openWebsite=Открытый сайт customServiceGroup.displayName=Группа услуг customServiceGroup.displayDescription=Сгруппируйте несколько сервисов в одну категорию -initScript=Запуск на shell init -shellScript=Сделать скрипт доступным во время сеанса оболочки -fileScript=Разрешить вызов скрипта с аргументами в виде файлов в браузере файлов +initScript=Init script - скрипт, запускаемый при инициализации оболочки +shellScript=Скрипт сеанса оболочки - сделать скрипт доступным для выполнения во время сеанса оболочки +runnableScript=Запускаемый скрипт - позволяет запускать скрипт прямо из концентратора соединений +fileScript=Файловый скрипт - позволяет вызывать скрипт с аргументами файла в файловом браузере runScript=Запуск скрипта copyUrl=Копировать URL fixedServiceGroup.displayName=Группа услуг @@ -153,6 +154,12 @@ fixedServiceGroup.displayDescription=Список доступных серви mappedService.displayName=Сервис mappedService.displayDescription=Взаимодействие с сервисом, открываемым контейнером customService.displayName=Сервис -customService.displayDescription=Добавьте пользовательский сервис для туннелирования и открытия +customService.displayDescription=Добавьте порт удаленного сервиса для туннелирования к вашей локальной машине fixedService.displayName=Сервис fixedService.displayDescription=Использовать предопределенный сервис +noServices=Нет доступных сервисов +hasServices=$COUNT$ доступные сервисы +hasService=$COUNT$ доступный сервис +openHttp=Открытый HTTP-сервис +openHttps=Открытая служба HTTPS +noScriptsAvailable=Нет доступных скриптов diff --git a/lang/base/strings/translations_tr.properties b/lang/base/strings/translations_tr.properties index 6a665bff3..ac72818cb 100644 --- a/lang/base/strings/translations_tr.properties +++ b/lang/base/strings/translations_tr.properties @@ -143,9 +143,10 @@ serviceHostDescription=Hizmetin üzerinde çalıştığı ana bilgisayar openWebsite=Açık web sitesi customServiceGroup.displayName=Hizmet grubu customServiceGroup.displayDescription=Birden fazla hizmeti tek bir kategoride gruplayın -initScript=Kabuk başlangıcında çalıştır -shellScript=Kabuk oturumu sırasında komut dosyasını kullanılabilir hale getirme -fileScript=Kodun dosya tarayıcısında dosya bağımsız değişkenleriyle çağrılmasına izin ver +initScript=Başlangıç betiği - Kabuk başlangıcında çalıştır +shellScript=Kabuk oturumu komut dosyası - Komut dosyasını kabuk oturumu sırasında çalıştırılabilir hale getirin +runnableScript=Çalıştırılabilir komut dosyası - Komut dosyasının doğrudan bağlantı hub'ından çalıştırılmasına izin ver +fileScript=Dosya betiği - Dosya tarayıcısında betiğin dosya argümanlarıyla çağrılmasına izin ver runScript=Komut dosyasını çalıştır copyUrl=URL'yi kopyala fixedServiceGroup.displayName=Hizmet grubu @@ -153,6 +154,12 @@ fixedServiceGroup.displayDescription=Bir sistemdeki mevcut hizmetleri listeleme mappedService.displayName=Hizmet mappedService.displayDescription=Bir konteyner tarafından sunulan bir hizmetle etkileşim customService.displayName=Hizmet -customService.displayDescription=Tünele özel bir hizmet ekleyin ve açın +customService.displayDescription=Yerel makinenize tünel açmak için bir uzak hizmet bağlantı noktası ekleyin fixedService.displayName=Hizmet fixedService.displayDescription=Önceden tanımlanmış bir hizmet kullanın +noServices=Mevcut hizmet yok +hasServices=$COUNT$ mevcut hi̇zmetler +hasService=$COUNT$ mevcut hizmet +openHttp=Açık HTTP hizmeti +openHttps=HTTPS hizmetini açın +noScriptsAvailable=Mevcut senaryo yok diff --git a/lang/base/strings/translations_zh.properties b/lang/base/strings/translations_zh.properties index e9973d8ba..1d8254d8c 100644 --- a/lang/base/strings/translations_zh.properties +++ b/lang/base/strings/translations_zh.properties @@ -143,9 +143,10 @@ serviceHostDescription=服务运行的主机 openWebsite=打开网站 customServiceGroup.displayName=服务组 customServiceGroup.displayDescription=将多项服务归为一类 -initScript=在 shell init 上运行 -shellScript=在 shell 会话中提供脚本 -fileScript=允许在文件浏览器中使用文件参数调用脚本 +initScript=初始脚本 - 在 shell 启动时运行 +shellScript=shell 会话脚本 - 在 shell 会话中运行脚本 +runnableScript=可运行脚本 - 允许从连接集线器直接运行脚本 +fileScript=文件脚本 - 允许在文件浏览器中使用文件参数调用脚本 runScript=运行脚本 copyUrl=复制 URL fixedServiceGroup.displayName=服务组 @@ -153,6 +154,12 @@ fixedServiceGroup.displayDescription=列出系统中可用的服务 mappedService.displayName=服务 mappedService.displayDescription=与容器暴露的服务交互 customService.displayName=服务 -customService.displayDescription=为隧道和开放添加自定义服务 +customService.displayDescription=添加远程服务端口,以隧道方式连接本地计算机 fixedService.displayName=服务 fixedService.displayDescription=使用预定义服务 +noServices=无可用服务 +hasServices=$COUNT$ 可用服务 +hasService=$COUNT$ 可用服务 +openHttp=开放式 HTTP 服务 +openHttps=打开 HTTPS 服务 +noScriptsAvailable=无脚本可用 diff --git a/lang/base/texts/executionType_da.md b/lang/base/texts/executionType_da.md index a9617f683..126d34517 100644 --- a/lang/base/texts/executionType_da.md +++ b/lang/base/texts/executionType_da.md @@ -2,15 +2,15 @@ Du kan bruge et script i flere forskellige scenarier. -Når du aktiverer et script, dikterer udførelsestyperne, hvad XPipe vil gøre med scriptet. +Når du aktiverer et script via dets aktiveringsknap, dikterer udførelsestyperne, hvad XPipe vil gøre med scriptet. -## Init-scripts +## Init-script-type -Når et script er angivet som init-script, kan det vælges i shell-miljøer. +Når et script er angivet som init-script, kan det vælges i shell-miljøer til at blive kørt ved init. Hvis et script er aktiveret, vil det desuden automatisk blive kørt ved init i alle kompatible shells. -Hvis du f.eks. opretter et simpelt init-script som +Hvis du f.eks. opretter et simpelt init-script med ``` alias ll="ls -l" alias la="ls -A" @@ -18,33 +18,43 @@ alias l="ls -CF" ``` du vil have adgang til disse aliasser i alle kompatible shell-sessioner, hvis scriptet er aktiveret. -## Shell-scripts +## Kørbar script-type -Et normalt shell-script er beregnet til at blive kaldt i en shell-session i din terminal. -Når det er aktiveret, bliver scriptet kopieret til målsystemet og lagt ind i PATH i alle kompatible shells. -På den måde kan du kalde scriptet fra hvor som helst i en terminalsession. +Et kørbart shell-script er beregnet til at blive kaldt for en bestemt forbindelse fra forbindelseshubben. +Når dette script er aktiveret, vil scriptet være tilgængeligt for kald fra scripts-knappen for en forbindelse med en kompatibel shell-dialekt. + +Hvis du f.eks. opretter et simpelt `sh`-dialekt-shellscript med navnet `ps` for at vise den aktuelle procesliste med +``` +ps -A +``` +kan du kalde scriptet på enhver kompatibel forbindelse i menuen scripts. + +## Fil script type + +Endelig kan du også køre brugerdefinerede scripts med filinput fra filbrowserens grænseflade. +Når et filscript er aktiveret, vises det i filbrowseren, så det kan køres med filinput. + +Hvis du f.eks. opretter et simpelt filscript med +``` +diff "$1" "$2" +``` +kan du køre scriptet på udvalgte filer, hvis scriptet er aktiveret. +I dette eksempel vil scriptet kun køre, hvis du har valgt præcis to filer. +Ellers vil diff-kommandoen mislykkes. + +## Shell-session script type + +Et sessionsscript er beregnet til at blive kaldt i en shell-session i din terminal. +Når det er aktiveret, vil scriptet blive kopieret til målsystemet og lagt i PATH i alle kompatible shells. +På den måde kan du kalde scriptet hvor som helst i en terminalsession. Scriptnavnet skrives med små bogstaver, og mellemrum erstattes med understregninger, så du nemt kan kalde scriptet. -Hvis du f.eks. opretter et simpelt shell-script med navnet `apti` som +Hvis du for eksempel opretter et simpelt shellscript til `sh`-dialekter ved navn `apti` med ``` sudo apt install "$1" ``` -kan du kalde det på ethvert kompatibelt system med `apti.sh `, hvis scriptet er aktiveret. - -## Fil-scripts - -Endelig kan du også køre brugerdefinerede scripts med filinput fra filbrowser-grænsefladen. -Når et filscript er aktiveret, vises det i filbrowseren, så det kan køres med filinput. - -Hvis du f.eks. opretter et simpelt filscript som -``` -sudo apt install "$@" -``` -kan du køre scriptet på udvalgte filer, hvis scriptet er aktiveret. +kan du kalde scriptet på ethvert kompatibelt system med `apti.sh ` i en terminalsession, hvis scriptet er aktiveret. ## Flere typer -Da eksemplet på fil-scriptet er det samme som eksemplet på shell-scriptet ovenfor, -kan du se, at du også kan sætte kryds i flere bokse for udførelsestyper af et script, hvis de skal bruges i flere scenarier. - - +Du kan også markere flere felter for udførelsestyper af et script, hvis de skal bruges i flere scenarier. diff --git a/lang/base/texts/executionType_de.md b/lang/base/texts/executionType_de.md index cc16f49e2..1a5049955 100644 --- a/lang/base/texts/executionType_de.md +++ b/lang/base/texts/executionType_de.md @@ -2,15 +2,15 @@ Du kannst ein Skript in vielen verschiedenen Szenarien verwenden. -Wenn du ein Skript aktivierst, legen die Ausführungsarten fest, was XPipe mit dem Skript tun soll. +Wenn du ein Skript über die Schaltfläche "Aktivieren" aktivierst, legen die Ausführungsarten fest, was XPipe mit dem Skript tun soll. -## Init-Skripte +## Init-Skripttyp -Wenn ein Skript als Init-Skript gekennzeichnet ist, kann es in Shell-Umgebungen ausgewählt werden. +Wenn ein Skript als Init-Skript gekennzeichnet ist, kann es in Shell-Umgebungen ausgewählt werden, um bei Init ausgeführt zu werden. -Wenn ein Skript aktiviert ist, wird es außerdem automatisch bei init in allen kompatiblen Shells ausgeführt. +Wenn ein Skript aktiviert ist, wird es außerdem in allen kompatiblen Shells automatisch bei init ausgeführt. -Wenn du zum Beispiel ein einfaches Init-Skript erstellst wie +Wenn du zum Beispiel ein einfaches Init-Skript mit ``` alias ll="ls -l" alias la="ls -A" @@ -18,33 +18,43 @@ alias l="ls -CF" ``` hast du in allen kompatiblen Shell-Sitzungen Zugang zu diesen Aliasen, wenn das Skript aktiviert ist. -## Shell-Skripte +## Lauffähiger Skripttyp -Ein normales Shell-Skript ist dafür gedacht, in einer Shell-Sitzung in deinem Terminal aufgerufen zu werden. -Wenn es aktiviert ist, wird das Skript auf das Zielsystem kopiert und in den PATH aller kompatiblen Shells aufgenommen. -So kannst du das Skript von überall in einer Terminal-Sitzung aufrufen. -Der Skriptname wird kleingeschrieben und Leerzeichen werden durch Unterstriche ersetzt, damit du das Skript leicht aufrufen kannst. +Ein lauffähiges Shell-Skript ist dafür gedacht, für eine bestimmte Verbindung vom Verbindungs-Hub aus aufgerufen zu werden. +Wenn dieses Skript aktiviert ist, kann das Skript über die Schaltfläche Skripte für eine Verbindung mit einem kompatiblen Shell-Dialekt aufgerufen werden. -Wenn du zum Beispiel ein einfaches Shell-Skript mit dem Namen `apti` wie folgt erstellst +Wenn du zum Beispiel ein einfaches Shell-Skript im `sh`-Dialekt mit dem Namen `ps` erstellst, um die aktuelle Prozessliste anzuzeigen mit ``` -sudo apt install "$1" +ps -A ``` -kannst du das auf jedem kompatiblen System mit `apti.sh ` aufrufen, wenn das Skript aktiviert ist. +kannst du das Skript auf jeder kompatiblen Verbindung im Menü Skripte aufrufen. -## Datei-Skripte +## Datei-Skripttyp Schließlich kannst du auch benutzerdefinierte Skripte mit Dateieingaben über die Dateibrowser-Schnittstelle ausführen. Wenn ein Dateiskript aktiviert ist, wird es im Dateibrowser angezeigt und kann mit Dateieingaben ausgeführt werden. -Wenn du zum Beispiel ein einfaches Dateiskript erstellst wie +Wenn du zum Beispiel ein einfaches Dateiskript erstellst mit ``` -sudo apt install "$@" +diff "$1" "$2" ``` -kannst du das Skript für ausgewählte Dateien ausführen, wenn das Skript aktiviert ist. +erstellst, kannst du das Skript für ausgewählte Dateien ausführen, wenn das Skript aktiviert ist. +In diesem Beispiel wird das Skript nur dann erfolgreich ausgeführt, wenn du genau zwei Dateien ausgewählt hast. +Andernfalls wird der Befehl diff fehlschlagen. + +## Shell-Sitzung Skripttyp + +Ein Sitzungsskript ist dafür gedacht, in einer Shell-Sitzung in deinem Terminal aufgerufen zu werden. +Wenn es aktiviert ist, wird das Skript auf das Zielsystem kopiert und in den PATH aller kompatiblen Shells aufgenommen. +So kannst du das Skript von überall in einer Terminalsitzung aufrufen. +Der Skriptname wird kleingeschrieben und Leerzeichen werden durch Unterstriche ersetzt, damit du das Skript leicht aufrufen kannst. + +Wenn du zum Beispiel ein einfaches Shell-Skript für `sh`-Dialekte namens `apti` mit +``` +sudo apt install "$1" +``` +kannst du das Skript auf jedem kompatiblen System mit `apti.sh ` in einer Terminalsitzung aufrufen, wenn das Skript aktiviert ist. ## Mehrere Typen -Da das Beispielskript für die Datei dasselbe ist wie das Beispielsskript für die Shell oben, -siehst du, dass du auch mehrere Kästchen für die Ausführungsarten eines Skripts ankreuzen kannst, wenn sie in mehreren Szenarien verwendet werden sollen. - - +Du kannst auch mehrere Kästchen für die Ausführungsarten eines Skripts ankreuzen, wenn sie in mehreren Szenarien verwendet werden sollen. diff --git a/lang/base/texts/executionType_en.md b/lang/base/texts/executionType_en.md index 7a0dbec4e..df92a4c02 100644 --- a/lang/base/texts/executionType_en.md +++ b/lang/base/texts/executionType_en.md @@ -2,15 +2,15 @@ You can use a script in multiple different scenarios. -When enabling a script, the execution types dictate what XPipe will do with the script. +When enabling a script via its enable toggle button, the execution types dictate what XPipe will do with the script. -## Init scripts +## Init script type -When a script is designated as init script, it can be selected in shell environments. +When a script is designated as init script, it can be selected in shell environments to be run on init. Furthermore, if a script is enabled, it will automatically be run on init in all compatible shells. -For example, if you create a simple init script like +For example, if you create a simple init script with ``` alias ll="ls -l" alias la="ls -A" @@ -18,33 +18,43 @@ alias l="ls -CF" ``` you will have access to these aliases in all compatible shell sessions if the script is enabled. -## Shell scripts +## Runnable script type -A normal shell script is intended to be called in a shell session in your terminal. -When enabled, the script will be copied to the target system and put into the PATH in all compatible shells. -This allows you to call the script from anywhere in a terminal session. -The script name will be lowercased and spaces will be replaced with underscores, allowing you to easily call the script. +A runnable shell script is intended to be called for a certain connection from the connection hub. +When this script is enabled, the script will be available to call from the scripts button for a connection with a compatible shell dialect. -For example, if you create a simple shell script named `apti` like +For example, if you create a simple `sh` dialect shell script named `ps` to show the current process list with ``` -sudo apt install "$1" +ps -A ``` -you can call that on any compatible system with `apti.sh ` if the script is enabled. +you can call the script on any compatible connection in the scripts menu. -## File scripts +## File script type Lastly, you can also run custom script with file inputs from the file browser interface. When a file script is enabled, it will show up in the file browser to be run with file inputs. -For example, if you create a simple file script like +For example, if you create a simple file script with ``` -sudo apt install "$@" +diff "$1" "$2" ``` you can run the script on selected files if the script is enabled. +In this example, the script will only run successfully if you have exactly two files selected. +Otherwise, the diff command will fail. + +## Shell session script type + +A session script is intended to be called in a shell session in your terminal. +When enabled, the script will be copied to the target system and put into the PATH in all compatible shells. +This allows you to call the script from anywhere in a terminal session. +The script name will be lowercased and spaces will be replaced with underscores, allowing you to easily call the script. + +For example, if you create a simple shell script for `sh` dialects named `apti` with +``` +sudo apt install "$1" +``` +you can call the script on any compatible system with `apti.sh ` in a terminal session if the script is enabled. ## Multiple types -As the sample file script is the same as the sample shell script above, -you see that you can also tick multiple boxes for execution types of a script if they should be used in multiple scenarios. - - +You can also tick multiple boxes for execution types of a script if they should be used in multiple scenarios. diff --git a/lang/base/texts/executionType_es.md b/lang/base/texts/executionType_es.md index 5cc3852c9..72ac9ae3d 100644 --- a/lang/base/texts/executionType_es.md +++ b/lang/base/texts/executionType_es.md @@ -2,15 +2,15 @@ Puedes utilizar un script en múltiples escenarios diferentes. -Al activar un script, los tipos de ejecución dictan lo que XPipe hará con el script. +Al activar un script mediante su botón de activación, los tipos de ejecución dictan lo que XPipe hará con el script. -## Guiones de inicio +## Tipo de script de inicio -Cuando un script se designa como script init, se puede seleccionar en entornos shell. +Cuando un script se designa como script init, se puede seleccionar en entornos shell para que se ejecute en init. Además, si un script está activado, se ejecutará automáticamente en init en todos los shells compatibles. -Por ejemplo, si creas un script init sencillo como +Por ejemplo, si creas un script init simple con ``` alias ll="ls -l" alias la="ls -A" @@ -18,33 +18,43 @@ alias l="ls -CF" ``` tendrás acceso a estos alias en todas las sesiones de shell compatibles si el script está activado. -## Scripts de shell +## Tipo de script ejecutable -Un script de shell normal está pensado para ser llamado en una sesión de shell en tu terminal. -Cuando está activado, el script se copiará en el sistema de destino y se pondrá en el PATH en todas las shell compatibles. +Un script de shell ejecutable está destinado a ser llamado para una determinada conexión desde el concentrador de conexiones. +Cuando este script está habilitado, el script estará disponible para ser llamado desde el botón de scripts para una conexión con un dialecto shell compatible. + +Por ejemplo, si creas un sencillo script de shell de dialecto `sh` llamado `ps` para mostrar la lista de procesos actuales con +``` +ps -A +``` +puedes llamar al script en cualquier conexión compatible en el menú scripts. + +## Archivo tipo script + +Por último, también puedes ejecutar scripts personalizados con entradas de archivo desde la interfaz del explorador de archivos. +Cuando se habilita un script de archivo, aparecerá en el explorador de archivos para ser ejecutado con entradas de archivo. + +Por ejemplo, si creas un script de archivo simple con +``` +diff "$1" "$2" +``` +puedes ejecutar el script en los archivos seleccionados si el script está activado. +En este ejemplo, el script sólo se ejecutará correctamente si tienes exactamente dos archivos seleccionados. +De lo contrario, el comando diff fallará. + +## Sesión de shell tipo script + +Un script de sesión está pensado para ser llamado en una sesión shell en tu terminal. +Cuando está activado, el script se copiará en el sistema de destino y se pondrá en el PATH en todos los shells compatibles. Esto te permite llamar al script desde cualquier lugar de una sesión de terminal. El nombre del script se escribirá en minúsculas y los espacios se sustituirán por guiones bajos, lo que te permitirá llamarlo fácilmente. -Por ejemplo, si creas un sencillo script de shell llamado `apti` como +Por ejemplo, si creas un script de shell sencillo para dialectos `sh` llamado `apti` con ``` sudo apt install "$1" ``` -puedes invocarlo en cualquier sistema compatible con `apti.sh ` si el script está activado. - -## Archivo scripts - -Por último, también puedes ejecutar scripts personalizados con entradas de archivo desde la interfaz del explorador de archivos. -Cuando un script de archivo esté activado, aparecerá en el explorador de archivos para ejecutarse con entradas de archivo. - -Por ejemplo, si creas un script de archivo sencillo como -``` -sudo apt install "$@" -``` -puedes ejecutar el script en los archivos seleccionados si el script está activado. +puedes llamar al script en cualquier sistema compatible con `apti.sh ` en una sesión de terminal si el script está activado. ## Tipos múltiples -Como el script de archivo de ejemplo es el mismo que el script de shell de ejemplo anterior, -verás que también puedes marcar varias casillas para los tipos de ejecución de un script si deben utilizarse en varios escenarios. - - +También puedes marcar varias casillas para los tipos de ejecución de un script si deben utilizarse en varios escenarios. diff --git a/lang/base/texts/executionType_fr.md b/lang/base/texts/executionType_fr.md index 63d1f1b99..9ed9ffc74 100644 --- a/lang/base/texts/executionType_fr.md +++ b/lang/base/texts/executionType_fr.md @@ -2,15 +2,15 @@ Tu peux utiliser un script dans plusieurs scénarios différents. -Lors de l'activation d'un script, les types d'exécution dictent ce que XPipe fera avec le script. +Lors de l'activation d'un script via son bouton bascule d'activation, les types d'exécution dictent ce que XPipe fera avec le script. -## Init scripts +## Type de script d'initialisation -Lorsqu'un script est désigné comme script init, il peut être sélectionné dans les environnements shell. +Lorsqu'un script est désigné comme script init, il peut être sélectionné dans les environnements shell pour être exécuté lors de l'init. -De plus, si un script est activé, il sera automatiquement exécuté lors de l'init dans tous les shells compatibles. +De plus, si un script est activé, il sera automatiquement exécuté lors de l'initialisation dans tous les shells compatibles. -Par exemple, si tu crées un script init simple comme +Par exemple, si tu crées un script init simple avec ``` alias ll="ls -l" alias la="ls -A" @@ -18,33 +18,43 @@ alias l="ls -CF" ``` tu auras accès à ces alias dans toutes les sessions shell compatibles si le script est activé. -## Scripts shell +## Type de script exécutable -Un script shell normal est destiné à être appelé dans une session shell dans ton terminal. -Lorsqu'il est activé, le script sera copié sur le système cible et placé dans le chemin d'accès (PATH) de tous les shells compatibles. -Cela te permet d'appeler le script depuis n'importe quel endroit d'une session de terminal. -Le nom du script sera en minuscules et les espaces seront remplacés par des traits de soulignement, ce qui te permettra d'appeler facilement le script. +Un script shell exécutable est destiné à être appelé pour une certaine connexion à partir du hub de connexion. +Lorsque ce script est activé, il sera possible de l'appeler à partir du bouton scripts pour une connexion avec un dialecte shell compatible. -Par exemple, si tu crées un script shell simple nommé `apti` comme suit +Par exemple, si tu crées un simple script shell de dialecte `sh` nommé `ps` pour afficher la liste des processus en cours avec ``` -sudo apt install "$1" +ps -A ``` -vous pouvez l'appeler sur n'importe quel système compatible avec `apti.sh ` si le script est activé. +tu peux appeler le script sur n'importe quelle connexion compatible dans le menu des scripts. -## Fichier scripts +## Fichier type de script Enfin, tu peux aussi exécuter des scripts personnalisés avec des entrées de fichiers à partir de l'interface du navigateur de fichiers. Lorsqu'un script de fichier est activé, il s'affiche dans le navigateur de fichiers pour être exécuté avec des entrées de fichier. -Par exemple, si tu crées un script de fichier simple comme +Par exemple, si tu crées un script de fichier simple avec ``` -sudo apt install "$@" +diff "$1" "$2" ``` tu peux exécuter le script sur les fichiers sélectionnés si le script est activé. +Dans cet exemple, le script ne s'exécutera avec succès que si tu as exactement deux fichiers sélectionnés. +Sinon, la commande diff échouera. + +## Session shell type de script + +Un script de session est destiné à être appelé dans une session shell dans ton terminal. +Lorsqu'il est activé, le script est copié sur le système cible et placé dans le chemin d'accès (PATH) de tous les shells compatibles. +Cela te permet d'appeler le script depuis n'importe quel endroit d'une session de terminal. +Le nom du script sera en minuscules et les espaces seront remplacés par des traits de soulignement, ce qui te permettra d'appeler facilement le script. + +Par exemple, si tu crées un script shell simple pour les dialectes `sh` nommé `apti` avec +``` +sudo apt install "$1" +``` +tu peux appeler le script sur n'importe quel système compatible avec `apti.sh ` dans une session de terminal si le script est activé. ## Plusieurs types -Comme l'exemple de script de fichier est le même que l'exemple de script shell ci-dessus, -tu vois que tu peux aussi cocher plusieurs cases pour les types d'exécution d'un script s'ils doivent être utilisés dans plusieurs scénarios. - - +Tu peux aussi cocher plusieurs cases pour les types d'exécution d'un script s'ils doivent être utilisés dans plusieurs scénarios. diff --git a/lang/base/texts/executionType_it.md b/lang/base/texts/executionType_it.md index d3c07a921..ba1553f04 100644 --- a/lang/base/texts/executionType_it.md +++ b/lang/base/texts/executionType_it.md @@ -2,15 +2,15 @@ Puoi utilizzare uno script in diversi scenari. -Quando abiliti uno script, i tipi di esecuzione stabiliscono cosa XPipe farà con lo script. +Quando abiliti uno script tramite il pulsante di attivazione, i tipi di esecuzione stabiliscono cosa XPipe farà con lo script. -## Script di avvio +## Tipo di script iniziale -Quando uno script è designato come script di avvio, può essere selezionato negli ambienti shell. +Quando uno script è designato come script di avvio, può essere selezionato negli ambienti shell per essere eseguito all'avvio. Inoltre, se uno script è abilitato, verrà eseguito automaticamente all'avvio in tutte le shell compatibili. -Ad esempio, se crei un semplice script di avvio come +Ad esempio, se crei un semplice script di avvio con ``` alias ll="ls -l" alias la="ls -A" @@ -18,33 +18,43 @@ alias l="ls -CF" ``` avrai accesso a questi alias in tutte le sessioni di shell compatibili se lo script è abilitato. -## Script di shell +## Tipo di script eseguibile -Un normale script di shell è destinato a essere richiamato in una sessione di shell nel tuo terminale. -Se abilitato, lo script verrà copiato sul sistema di destinazione e inserito nel PATH di tutte le shell compatibili. +Uno script di shell eseguibile è destinato a essere richiamato per una determinata connessione dall'hub di connessione. +Quando questo script è abilitato, sarà disponibile per essere richiamato dal pulsante script per una connessione con un dialetto shell compatibile. + +Ad esempio, se crei un semplice script di shell in dialetto `sh` chiamato `ps` per mostrare l'elenco dei processi correnti con +``` +ps -A +``` +puoi richiamare lo script su qualsiasi connessione compatibile nel menu degli script. + +## Tipo di file script + +Infine, puoi anche eseguire script personalizzati con input da file dall'interfaccia del browser dei file. +Quando un file script è abilitato, viene visualizzato nel browser dei file per essere eseguito con gli input dei file. + +Ad esempio, se crei un semplice file script con +``` +diff "$1" "$2" +``` +puoi eseguire lo script sui file selezionati se lo script è abilitato. +In questo esempio, lo script verrà eseguito correttamente solo se sono stati selezionati esattamente due file. +In caso contrario, il comando diff fallirà. + +## Tipo di script della sessione di shell + +Uno script di sessione è destinato a essere richiamato in una sessione di shell nel tuo terminale. +Se abilitato, lo script verrà copiato sul sistema di destinazione e inserito nel PATH in tutte le shell compatibili. Questo ti permette di richiamare lo script da qualsiasi punto di una sessione di terminale. Il nome dello script sarà minuscolo e gli spazi saranno sostituiti da trattini bassi, consentendoti di richiamare facilmente lo script. -Ad esempio, se crei un semplice script di shell chiamato `apti` come +Ad esempio, se crei un semplice script di shell per i dialetti `sh` chiamato `apti` con ``` sudo apt install "$1" ``` -puoi richiamarlo su qualsiasi sistema compatibile con `apti.sh ` se lo script è abilitato. - -## File script - -Infine, puoi anche eseguire script personalizzati con input da file dall'interfaccia del browser dei file. -Quando uno script di file è abilitato, viene visualizzato nel browser dei file per essere eseguito con input di file. - -Ad esempio, se crei un semplice script di file come -``` -sudo apt install "$@" -``` -puoi eseguire lo script sui file selezionati se lo script è abilitato. +puoi richiamare lo script su qualsiasi sistema compatibile con `apti.sh ` in una sessione di terminale se lo script è abilitato. ## Tipi multipli -Poiché lo script di esempio per i file è identico allo script di esempio per la shell di cui sopra, -puoi anche spuntare più caselle per i tipi di esecuzione di uno script se questi devono essere utilizzati in più scenari. - - +Puoi anche spuntare più caselle per i tipi di esecuzione di uno script se devono essere utilizzati in più scenari. diff --git a/lang/base/texts/executionType_ja.md b/lang/base/texts/executionType_ja.md index 56656bec9..c1cfd79ad 100644 --- a/lang/base/texts/executionType_ja.md +++ b/lang/base/texts/executionType_ja.md @@ -2,15 +2,15 @@ スクリプトは複数の異なるシナリオで使用できる。 -スクリプトを有効にする場合、実行タイプによってXPipeがスクリプトで何を行うかが決まる。 +有効化トグルボタンでスクリプトを有効にすると、実行タイプによってXPipeがスクリプトに対して何を行うかが決まる。 -## スクリプトの初期化 +## スクリプトの初期タイプ -スクリプトをinitスクリプトとして指定すると、シェル環境で選択できるようになる。 +スクリプトをinitスクリプトとして指定すると、シェル環境でinit時に実行されるスクリプトを選択できる。 -さらに、スクリプトが有効になっていれば、互換性のあるすべてのシェルで、init時に自動的に実行される。 +さらに、スクリプトを有効にすると、互換性のあるすべてのシェルで、 init時に自動的に実行される。 -例えば、次のような単純なinitスクリプトを作成した場合 +例えば、単純なinitスクリプトを ``` alias ll="ls -l" alias la="ls -A" @@ -18,33 +18,43 @@ alias l="ls -CF" ``` スクリプトが有効になっていれば、互換性のあるすべてのシェル・セッションでこれらのエイリアスにアクセスできる。 -## シェルスクリプト +## 実行可能なスクリプトタイプ -通常のシェルスクリプトは、ターミナル上のシェルセッションで呼び出され ることを想定している。 -有効にすると、スクリプトはターゲットシステムにコピーされ、 すべての互換シェルでPATHに入れられる。 -これにより、ターミナル・セッションのどこからでもスクリプトを呼び出すことができる。 +実行可能なシェルスクリプトは、接続ハブから特定の接続に対して呼び出される ことを意図している。 +このスクリプトが有効になっている場合, 互換性のあるシェル弁を持つ接続に対して, スクリプトボタンからスクリプトを呼び出すことができるようになる. + +例えば、単純な `sh` 方言のシェルスクリプトを `ps` という名前で作成し、現在のプロセスリストを表示するために +``` +ps -A +``` +とすれば、互換性のある接続であれば、スクリプトメニューからそのスクリプトを呼び出すことができる。 + +## ファイルスクリプトの種類 + +最後に、カスタムスクリプトをファイルブラウザのインターフェイスからファイル入力で実行することもできる。 +ファイルスクリプトが有効になると、ファイルブラウザに表示され、ファイル入力で実行できるようになる。 + +例えば、単純なファイルスクリプトを +``` +diff "$1" "$2" +``` +というスクリプトを作成すると、 スクリプトが有効になっていれば、選択されたファイルに対して スクリプトを実行することができる。 +この例では、ちょうど2つのファイルが選択されているときだけ、 スクリプトは正常に実行される。 +そうでない場合、diffコマンドは失敗する。 + +## シェルセッションのスクリプトタイプ + +セッションスクリプトは、端末のシェルセッションで呼び出すためのものである。 +有効にすると、スクリプトはターゲットシステムにコピーされ、互換性の あるすべてのシェルのPATHに入れられる。 +これにより、ターミナルセッションのどこからでもスクリプトを呼び出すことができる。 スクリプト名は小文字になり、スペースはアンダースコアに置き換えられるので、簡単にスクリプトを呼び出すことができる。 -例えば、次のような`apti`という単純なシェルスクリプトを作成した場合、次のようになる。 +例えば、`sh`方言用の簡単なシェルスクリプトを`apti`という名前で作成し、それを ``` sudo apt install "$1" ``` -スクリプトが有効になっていれば、互換性のあるシステム上で`apti.sh `を使ってそれを呼び出すことができる。 - -## ファイルスクリプト - -最後に、ファイルブラウザのインターフェイスからファイル入力を使ってカスタムスクリプトを実行することもできる。 -ファイルスクリプトが有効になると、ファイルブラウザに表示され、ファイル入力で実行できるようになる。 - -例えば、次のような簡単なファイルスクリプトを作成した場合 -``` -sudo apt install "$@" -``` -スクリプトが有効になっていれば、選択したファイルに対してスクリプトを実行できる。 +スクリプトが有効になっていれば、ターミナルセッションで`apti.sh `を使って互換性のあるシステム上でスクリプトを呼び出すことができる。 ## 複数のタイプ -ファイルスクリプトのサンプルは、上のシェルスクリプトのサンプルと同じである、 -スクリプトを複数のシナリオで使用する場合は、スクリプトの実行タイプに複数のチェックボックスを付けることもできる。 - - +スクリプトを複数のシナリオで使用する場合、スクリプトの実行タイプに複数のチェックを入れることもできる。 diff --git a/lang/base/texts/executionType_nl.md b/lang/base/texts/executionType_nl.md index c471f03d6..5faf96dd9 100644 --- a/lang/base/texts/executionType_nl.md +++ b/lang/base/texts/executionType_nl.md @@ -2,15 +2,15 @@ Je kunt een script in meerdere verschillende scenario's gebruiken. -Wanneer je een script inschakelt, bepalen de uitvoeringstypen wat XPipe met het script zal doen. +Wanneer je een script inschakelt via de inschakelknop, bepalen de uitvoeringstypen wat XPipe met het script zal doen. -## Init scripts +## Init script type -Als een script is aangewezen als init-script, kan het worden geselecteerd in shell-omgevingen. +Als een script is aangewezen als init-script, kan het in shell-omgevingen worden geselecteerd om bij init te worden uitgevoerd. -Bovendien, als een script is ingeschakeld, zal het automatisch worden uitgevoerd op init in alle compatibele shells. +Bovendien, als een script is ingeschakeld, zal het automatisch op init worden uitgevoerd in alle compatibele shells. -Als je bijvoorbeeld een eenvoudig init-script maakt als +Als je bijvoorbeeld een eenvoudig init-script maakt met ``` alias ll="ls -l" alias la="ls -A" @@ -18,33 +18,43 @@ alias l="ls -CF" ``` je hebt toegang tot deze aliassen in alle compatibele shell sessies als het script is ingeschakeld. -## Shell scripts +## Runnable scripttype -Een normaal shellscript is bedoeld om aangeroepen te worden in een shellsessie in je terminal. -Als dit is ingeschakeld, wordt het script gekopieerd naar het doelsysteem en in het PATH van alle compatibele shells gezet. -Hierdoor kun je het script overal vandaan in een terminalsessie aanroepen. -De scriptnaam wordt met kleine letters geschreven en spaties worden vervangen door underscores, zodat je het script gemakkelijk kunt aanroepen. +Een uitvoerbaar shellscript is bedoeld om te worden aangeroepen voor een bepaalde verbinding vanuit de verbindingshub. +Als dit script is ingeschakeld, is het script beschikbaar om aan te roepen via de scripts knop voor een verbinding met een compatibel shell dialect. -Als je bijvoorbeeld een eenvoudig shellscript maakt met de naam `apti` zoals +Als je bijvoorbeeld een eenvoudig `sh` dialect shellscript maakt met de naam `ps` om de huidige proceslijst te tonen met ``` -sudo apt install "$1" +ps -A ``` -kun je dat op elk compatibel systeem aanroepen met `apti.sh ` als het script is ingeschakeld. +kun je het script op elke compatibele verbinding aanroepen in het menu scripts. -## Bestandsscripts +## Type bestandsscript Tot slot kun je ook aangepaste scripts uitvoeren met bestandsinvoer vanuit de bestandsbrowserinterface. Als een bestandsscript is ingeschakeld, verschijnt het in de bestandsbrowser om te worden uitgevoerd met bestandsinvoer. -Als je bijvoorbeeld een eenvoudig bestandsscript maakt zoals +Als je bijvoorbeeld een eenvoudig bestandsscript maakt met ``` -sudo apt install "$@" +diff "$1" "$2" ``` kun je het script uitvoeren op geselecteerde bestanden als het script is ingeschakeld. +In dit voorbeeld zal het script alleen succesvol draaien als je precies twee bestanden hebt geselecteerd. +Anders zal het diff commando mislukken. + +## Shell sessie script type + +Een sessie script is bedoeld om aangeroepen te worden in een shell sessie in je terminal. +Als het is ingeschakeld, wordt het script gekopieerd naar het doelsysteem en in het PATH van alle compatibele shells gezet. +Hierdoor kun je het script overal vandaan in een terminalsessie aanroepen. +De scriptnaam wordt met kleine letters geschreven en spaties worden vervangen door underscores, zodat je het script gemakkelijk kunt aanroepen. + +Als je bijvoorbeeld een eenvoudig shellscript maakt voor `sh` dialecten met de naam `apti` met +``` +sudo apt install "$1" +``` +kun je het script op elk compatibel systeem aanroepen met `apti.sh ` in een terminal sessie als het script is ingeschakeld. ## Meerdere types -Aangezien het voorbeeldbestandsscript hetzelfde is als het voorbeeldshell-script hierboven, -zie je dat je ook meerdere vakjes kunt aanvinken voor uitvoeringstypen van een script als ze in meerdere scenario's moeten worden gebruikt. - - +Je kunt ook meerdere vakjes aanvinken voor uitvoeringstypen van een script als ze in meerdere scenario's moeten worden gebruikt. diff --git a/lang/base/texts/executionType_pt.md b/lang/base/texts/executionType_pt.md index fdd4c2fb9..30ebea0f3 100644 --- a/lang/base/texts/executionType_pt.md +++ b/lang/base/texts/executionType_pt.md @@ -2,15 +2,15 @@ Podes utilizar um script em vários cenários diferentes. -Ao ativar um script, os tipos de execução ditam o que o XPipe fará com o script. +Ao ativar um script através do respetivo botão de alternância, os tipos de execução determinam o que o XPipe fará com o script. -## Scripts de inicialização +## Tipo de script de inicialização -Quando um script é designado como script de inicialização, ele pode ser selecionado em ambientes shell. +Quando um script é designado como script init, ele pode ser selecionado em ambientes shell para ser executado no init. Além disso, se um script é habilitado, ele será automaticamente executado no init em todos os shells compatíveis. -Por exemplo, se criares um script de inicialização simples como +Por exemplo, se criares um script init simples com ``` alias ll="ls -l" alias la="ls -A" @@ -18,33 +18,43 @@ alias l="ls -CF" ``` terás acesso a estes aliases em todas as sessões de shell compatíveis se o script estiver ativado. -## Scripts de shell +## Tipo de script executável -Um script de shell normal destina-se a ser chamado numa sessão de shell no teu terminal. +Um script de shell executável destina-se a ser chamado para uma determinada ligação a partir do hub de ligação. +Quando este script está ativado, o script estará disponível para ser chamado a partir do botão de scripts para uma ligação com um dialeto de shell compatível. + +Por exemplo, se você criar um script de shell de dialeto `sh` simples chamado `ps` para mostrar a lista de processos atual com +``` +ps -A +``` +podes chamar o script em qualquer conexão compatível no menu de scripts. + +## Tipo de script de arquivo + +Por fim, também podes executar um script personalizado com entradas de ficheiro a partir da interface do navegador de ficheiros. +Quando um script de ficheiro está ativado, aparece no navegador de ficheiros para ser executado com entradas de ficheiro. + +Por exemplo, se criares um script de ficheiro simples com +``` +diff "$1" "$2" +``` +podes executar o script em ficheiros seleccionados se o script estiver ativado. +Neste exemplo, o script só será executado com êxito se tiveres exatamente dois arquivos selecionados. +Caso contrário, o comando diff falhará. + +## Tipo de script da sessão do shell + +Um script de sessão destina-se a ser chamado numa sessão de shell no teu terminal. Quando ativado, o script será copiado para o sistema alvo e colocado no PATH em todas as shells compatíveis. Isto permite-te chamar o script a partir de qualquer lugar numa sessão de terminal. O nome do script será escrito em minúsculas e os espaços serão substituídos por sublinhados, permitindo-te chamar facilmente o script. -Por exemplo, se criares um script de shell simples chamado `apti` como +Por exemplo, se você criar um script de shell simples para dialetos `sh` chamado `apti` com ``` -sudo apt install "$1" +sudo apt instala "$1" ``` -podes chamar isso em qualquer sistema compatível com `apti.sh ` se o script estiver ativado. - -## Scripts de ficheiros - -Por último, também podes executar scripts personalizados com entradas de ficheiros a partir da interface do navegador de ficheiros. -Quando um script de arquivo estiver habilitado, ele aparecerá no navegador de arquivos para ser executado com entradas de arquivo. - -Por exemplo, se criares um script de arquivo simples como -``` -sudo apt install "$@" -``` -podes executar o script em ficheiros seleccionados se o script estiver ativado. - -## Vários tipos - -Como o script de arquivo de exemplo é o mesmo que o script de shell de exemplo acima, -vês que também podes assinalar várias caixas para os tipos de execução de um script, se estes tiverem de ser usados em vários cenários. +podes chamar o script em qualquer sistema compatível com `apti.sh ` numa sessão de terminal se o script estiver ativado. +## Múltiplos tipos +Também podes assinalar várias caixas para os tipos de execução de um script se estes tiverem de ser utilizados em vários cenários. diff --git a/lang/base/texts/executionType_ru.md b/lang/base/texts/executionType_ru.md index ab81514b3..eeffc645d 100644 --- a/lang/base/texts/executionType_ru.md +++ b/lang/base/texts/executionType_ru.md @@ -2,15 +2,15 @@ Ты можешь использовать скрипт в нескольких различных сценариях. -При включении скрипта типы выполнения определяют, что XPipe будет делать со скриптом. +Когда ты включаешь скрипт с помощью кнопки включения, типы выполнения определяют, что XPipe будет делать со скриптом. -## Начальные скрипты +## Тип начального сценария -Когда скрипт обозначен как init script, он может быть выбран в среде оболочки. +Когда скрипт обозначен как init script, он может быть выбран в shell-окружении для запуска при init. -Более того, если скрипт включен, он будет автоматически запускаться при init во всех совместимых оболочках. +Кроме того, если скрипт включен, он будет автоматически запускаться при init во всех совместимых оболочках. -Например, если ты создашь простой init-скрипт типа +Например, если ты создашь простой init-скрипт со значением ``` alias ll="ls -l" alias la="ls -A" @@ -18,33 +18,43 @@ alias l="ls -CF" ``` ты будешь иметь доступ к этим псевдонимам во всех совместимых сессиях оболочки, если скрипт включен. -## Скрипты оболочки +## Тип запускаемого скрипта -Обычный shell-скрипт предназначен для вызова в shell-сессии в твоем терминале. -При включении скрипта он будет скопирован в целевую систему и помещен в PATH во всех совместимых оболочках. +Запускаемый скрипт оболочки предназначен для вызова из хаба соединений для определенного соединения. +Когда этот скрипт включен, он будет доступен для вызова из кнопки scripts для соединения с совместимым диалектом оболочки. + +Например, если ты создашь простой shell-скрипт на диалекте `sh` с именем `ps` для отображения списка текущих процессов с +``` +ps -A +``` +ты сможешь вызвать этот скрипт на любом совместимом соединении в меню скриптов. + +## Тип файла скрипта + +Наконец, ты также можешь запускать пользовательский скрипт с файловыми входами из интерфейса браузера файлов. +Когда файловый скрипт включен, он будет отображаться в браузере файлов для запуска с файловыми входами. + +Например, если ты создашь простой файловый скрипт с +``` +diff "$1" "$2" +``` +ты сможешь запустить скрипт на выбранных файлах, если он включен. +В этом примере скрипт будет успешно запущен только в том случае, если у тебя выбрано ровно два файла. +В противном случае команда diff завершится неудачей. + +## Тип скрипта сеанса оболочки + +Сессионный скрипт предназначен для вызова в сеансе оболочки в твоем терминале. +Если его включить, скрипт будет скопирован в целевую систему и помещен в PATH во всех совместимых оболочках. Это позволит тебе вызывать скрипт из любого места терминальной сессии. Имя скрипта будет написано в нижнем регистре, а пробелы будут заменены на подчеркивания, что позволит тебе легко вызывать скрипт. -Например, если ты создашь простой shell-скрипт с именем `apti`, например +Например, если ты создашь простой shell-скрипт для диалектов `sh` под названием `apti` с ``` sudo apt install "$1" ``` -ты сможешь вызвать его на любой совместимой системе с помощью `apti.sh `, если скрипт включен. - -## Скрипты файлов - -Наконец, ты также можешь запускать пользовательские скрипты с файловыми входами из интерфейса файлового браузера. -Когда файловый скрипт включен, он будет отображаться в браузере файлов, чтобы его можно было запустить с файловыми входами. - -Например, если ты создашь простой файловый скрипт типа -``` -sudo apt install "$@" -``` -ты сможешь запускать скрипт на выбранных файлах, если он включен. +ты сможешь вызвать этот скрипт на любой совместимой системе с помощью `apti.sh ` в терминальной сессии, если скрипт включен. ## Несколько типов -Поскольку пример файлового скрипта такой же, как и пример shell-скрипта выше, -ты видишь, что также можешь поставить несколько галочек напротив типов выполнения скрипта, если он должен использоваться в нескольких сценариях. - - +Ты также можешь поставить несколько галочек напротив типов выполнения скрипта, если он должен использоваться в нескольких сценариях. diff --git a/lang/base/texts/executionType_tr.md b/lang/base/texts/executionType_tr.md index 0827d8112..bf8579fb2 100644 --- a/lang/base/texts/executionType_tr.md +++ b/lang/base/texts/executionType_tr.md @@ -2,15 +2,15 @@ Bir komut dosyasını birden fazla farklı senaryoda kullanabilirsiniz. -Bir komut dosyası etkinleştirilirken, yürütme türleri XPipe'ın komut dosyasıyla ne yapacağını belirler. +Bir komut dosyasını etkinleştirme geçiş düğmesi aracılığıyla etkinleştirirken, yürütme türleri XPipe'ın komut dosyasıyla ne yapacağını belirler. -## Başlangıç betikleri +## Başlangıç komut dosyası türü -Bir komut dosyası init komut dosyası olarak belirlendiğinde, kabuk ortamlarında seçilebilir. +Bir komut dosyası init komut dosyası olarak belirlendiğinde, init sırasında çalıştırılmak üzere kabuk ortamlarında seçilebilir. Ayrıca, bir betik etkinleştirilirse, tüm uyumlu kabuklarda otomatik olarak init'te çalıştırılacaktır. -Örneğin, aşağıdaki gibi basit bir init betiği oluşturursanız +Örneğin, basit bir init betiği oluşturursanız ``` alias ll="ls -l" alias la="ls -A" @@ -18,33 +18,43 @@ alias l="ls -CF" ``` betik etkinleştirilmişse, tüm uyumlu kabuk oturumlarında bu takma adlara erişebileceksiniz. -## Kabuk betikleri +## Çalıştırılabilir komut dosyası türü -Normal bir kabuk betiği, terminalinizdeki bir kabuk oturumunda çağrılmak üzere tasarlanmıştır. -Etkinleştirildiğinde, betik hedef sisteme kopyalanır ve tüm uyumlu kabuklarda PATH'e yerleştirilir. -Bu, betiği bir terminal oturumunun herhangi bir yerinden çağırmanıza olanak tanır. -Betik adı küçük harflerle yazılır ve boşluklar alt çizgi ile değiştirilir, böylece betiği kolayca çağırabilirsiniz. +Çalıştırılabilir bir kabuk betiği, bağlantı hub'ından belirli bir bağlantı için çağrılmak üzere tasarlanmıştır. +Bu komut dosyası etkinleştirildiğinde, komut dosyası, uyumlu bir kabuk lehçesine sahip bir bağlantı için komut dosyaları düğmesinden çağrılabilecektir. -Örneğin, `apti` adında aşağıdaki gibi basit bir kabuk betiği oluşturursanız +Örneğin, geçerli işlem listesini göstermek için `ps` adında basit bir `sh` dialect kabuk betiği oluşturursanız ``` -sudo apt install "$1" +ps -A ``` -betik etkinleştirilmişse bunu uyumlu herhangi bir sistemde `apti.sh ` ile çağırabilirsiniz. +komut dosyasını komut dosyaları menüsündeki herhangi bir uyumlu bağlantıda çağırabilirsiniz. -## Dosya komut dosyaları +## Dosya komut dosyası türü Son olarak, dosya tarayıcı arayüzünden dosya girdileriyle özel komut dosyası da çalıştırabilirsiniz. Bir dosya komut dosyası etkinleştirildiğinde, dosya girdileriyle çalıştırılmak üzere dosya tarayıcısında görünecektir. -Örneğin, aşağıdaki gibi basit bir dosya komut dosyası oluşturursanız +Örneğin, aşağıdakileri içeren basit bir dosya komut dosyası oluşturursanız ``` -sudo apt install "$@" +diff "$1" "$2" ``` -komut dosyası etkinleştirilmişse komut dosyasını seçilen dosyalar üzerinde çalıştırabilirsiniz. +komut dosyası etkinleştirilmişse komut dosyasını seçili dosyalar üzerinde çalıştırabilirsiniz. +Bu örnekte, komut dosyası yalnızca tam olarak iki dosya seçiliyse başarıyla çalışacaktır. +Aksi takdirde, diff komutu başarısız olacaktır. + +## Kabuk oturumu komut dosyası türü + +Bir oturum betiği, terminalinizdeki bir kabuk oturumunda çağrılmak üzere tasarlanmıştır. +Etkinleştirildiğinde, betik hedef sisteme kopyalanır ve tüm uyumlu kabuklarda PATH'e yerleştirilir. +Bu, betiği bir terminal oturumunun herhangi bir yerinden çağırmanıza olanak tanır. +Betik adı küçük harflerle yazılır ve boşluklar alt çizgilerle değiştirilir, böylece betiği kolayca çağırabilirsiniz. + +Örneğin, `sh` lehçeleri için `apti` adında basit bir kabuk betiği oluşturursanız +``` +sudo apt install "$1" +``` +betik etkinleştirilmişse, betiği herhangi bir uyumlu sistemde terminal oturumunda `apti.sh ` ile çağırabilirsiniz. ## Çoklu tipler -Örnek dosya betiği yukarıdaki örnek kabuk betiği ile aynıdır, -birden fazla senaryoda kullanılmaları gerekiyorsa, bir komut dosyasının yürütme türleri için birden fazla kutuyu da işaretleyebileceğinizi görürsünüz. - - +Birden fazla senaryoda kullanılmaları gerekiyorsa, bir komut dosyasının yürütme türleri için birden fazla kutuyu da işaretleyebilirsiniz. diff --git a/lang/base/texts/executionType_zh.md b/lang/base/texts/executionType_zh.md index 4e54998e3..07cd61408 100644 --- a/lang/base/texts/executionType_zh.md +++ b/lang/base/texts/executionType_zh.md @@ -2,15 +2,15 @@ 您可以在多种不同情况下使用脚本。 -启用脚本时,执行类型决定了 XPipe 将如何处理脚本。 +通过启用切换按钮启用脚本时,执行类型决定了 XPipe 将如何处理脚本。 -## 初始脚本 +## 初始脚本类型 -当脚本被指定为初始脚本时,它可以在 shell 环境中被选择。 +当脚本被指定为初始脚本时,可在 shell 环境中选择该脚本,以便在启动时运行。 -此外,如果脚本被启用,它将在所有兼容的 shell 中自动运行 init 脚本。 +此外,如果脚本被启用,它将在所有兼容的 shell 中自动在 init 时运行。 -例如,如果创建一个简单的启动脚本,如 +例如,如果创建一个简单的初始化脚本,使用 ``` 别名 ll="ls -l" alias la="ls -A" @@ -18,33 +18,43 @@ alias la="ls -A" ``` 如果脚本已启用,您就可以在所有兼容的 shell 会话中访问这些别名。 -##hell 脚本 +## 可运行脚本类型 -普通 shell 脚本用于在终端的 shell 会话中调用。 +可运行 shell 脚本用于从连接中心调用特定连接。 +启用该脚本后,该脚本可通过脚本按钮调用,用于兼容 shell 方言的连接。 + +例如,如果您创建了一个名为 `ps` 的简单 `sh` 方言 shell 脚本,用 +``` +ps -A +``` +就可以在脚本菜单中的任何兼容连接上调用该脚本。 + +## 文件脚本类型 + +最后,您还可以通过文件浏览器界面的文件输入运行自定义脚本。 +启用文件脚本后,该脚本将显示在文件浏览器中,可通过文件输入运行。 + +例如,如果你创建了一个简单的文件脚本,其中包含 +``` +diff "$1" "$2" +``` +的简单文件脚本,如果脚本已启用,就可以在选定的文件上运行该脚本。 +在这个示例中,只有当你正好选择了两个文件时,脚本才能成功运行。 +否则,diff 命令将失败。 + +##hell 会话脚本类型 + +会话脚本用于在终端的 shell 会话中调用。 启用后,脚本将被复制到目标系统,并放入所有兼容 shell 的 PATH 中。 -这样就可以在终端会话的任何地方调用脚本。 +这样,你就可以在终端会话的任何地方调用脚本。 脚本名称将小写,空格将用下划线代替,以便于调用脚本。 -例如,如果创建一个名为 `apti` 的简单 shell 脚本,如 +例如,如果您为`sh`方言创建了一个简单的 shell 脚本,名为`apti`,其中包含 ``` sudo apt install "$1" ``` -如果脚本已启用,你就可以在任何兼容系统上使用 `apti.sh ` 调用该脚本。 - -## 文件脚本 - -最后,你还可以通过文件浏览器界面的文件输入运行自定义脚本。 -启用文件脚本后,它将显示在文件浏览器中,可通过文件输入运行。 - -例如,如果你创建了一个简单的文件脚本,如 -``` -sudo apt install "$@" -``` -这样的简单文件脚本,如果脚本已启用,你就可以在选定的文件上运行该脚本。 +如果脚本已启用,你就可以在终端会话中使用 `apti.sh ` 在任何兼容系统上调用该脚本。 ## 多种类型 -由于示例文件脚本与上述示例 shell 脚本相同、 -你可以看到,如果脚本应在多种情况下使用,你也可以为脚本的执行类型勾选多个复选框。 - - +如果脚本需要在多种情况下使用,还可以勾选脚本执行类型的多个复选框。 diff --git a/lang/proc/strings/fixed_en.properties b/lang/proc/strings/fixed_en.properties index ad78a823a..2bace7e8f 100644 --- a/lang/proc/strings/fixed_en.properties +++ b/lang/proc/strings/fixed_en.properties @@ -1,3 +1,14 @@ wsl=Windows Subsystem for Linux docker=Docker -proxmox=Proxmox PVE \ No newline at end of file +proxmox=Proxmox PVE +xonXoff=XON/XOFF +rtsCts=RTS/CTS +dsrDtr=DSR/DTR +putty=PuTTY +screen=Screen +minicom=Minicom +odd=Odd +even=Even +mark=Mark +space=Space +teleport=Teleport \ No newline at end of file diff --git a/lang/proc/strings/translations_da.properties b/lang/proc/strings/translations_da.properties index 526340f7f..a07f5392d 100644 --- a/lang/proc/strings/translations_da.properties +++ b/lang/proc/strings/translations_da.properties @@ -359,3 +359,33 @@ k8sPodActions=Pod-handlinger openVnc=Sæt VNC op commandGroup.displayName=Kommandogruppe commandGroup.displayDescription=Grupper tilgængelige kommandoer for et system +serial.displayName=Seriel forbindelse +serial.displayDescription=Åbn en seriel forbindelse i en terminal +serialPort=Seriel port +serialPortDescription=Den serielle port / enhed, der skal forbindes til +baudRate=Baud-hastighed +dataBits=Data-bits +stopBits=Stop-bits +parity=Paritet +flowControlWindow=Flow-kontrol +serialImplementation=Seriel implementering +serialImplementationDescription=Det værktøj, der skal bruges til at oprette forbindelse til den serielle port +serialHost=Vært +serialHostDescription=Systemet til at få adgang til den serielle port på +serialPortConfiguration=Konfiguration af seriel port +serialPortConfigurationDescription=Konfigurationsparametre for den tilsluttede serielle enhed +serialInformation=Seriel information +openXShell=Åbn i XShell +tsh.displayName=Teleport +tsh.displayDescription=Opret forbindelse til dine teleport-noder via tsh +tshNode.displayName=Teleport-knudepunkt +tshNode.displayDescription=Opret forbindelse til en teleport-node i en klynge +teleportCluster=Klynge +teleportClusterDescription=Den klynge, noden befinder sig i +teleportProxy=Proxy +teleportProxyDescription=Den proxyserver, der bruges til at oprette forbindelse til noden +teleportHost=Vært +teleportHostDescription=Værtsnavnet på noden +teleportUser=Bruger +teleportUserDescription=Den bruger, du skal logge ind som +login=Login diff --git a/lang/proc/strings/translations_de.properties b/lang/proc/strings/translations_de.properties index 8d8b8cc05..849b4264f 100644 --- a/lang/proc/strings/translations_de.properties +++ b/lang/proc/strings/translations_de.properties @@ -337,3 +337,33 @@ k8sPodActions=Pod-Aktionen openVnc=VNC einrichten commandGroup.displayName=Befehlsgruppe commandGroup.displayDescription=Verfügbare Befehle für ein System gruppieren +serial.displayName=Serielle Verbindung +serial.displayDescription=Eine serielle Verbindung in einem Terminal öffnen +serialPort=Serieller Anschluss +serialPortDescription=Der serielle Anschluss/das Gerät, mit dem eine Verbindung hergestellt werden soll +baudRate=Baudrate +dataBits=Datenbits +stopBits=Stoppbits +parity=Parität +flowControlWindow=Flusskontrolle +serialImplementation=Serielle Implementierung +serialImplementationDescription=Das Tool für die Verbindung mit der seriellen Schnittstelle +serialHost=Host +serialHostDescription=Das System für den Zugriff auf die serielle Schnittstelle auf +serialPortConfiguration=Konfiguration der seriellen Schnittstelle +serialPortConfigurationDescription=Konfigurationsparameter des angeschlossenen seriellen Geräts +serialInformation=Serielle Informationen +openXShell=In XShell öffnen +tsh.displayName=Teleport +tsh.displayDescription=Verbinde dich mit deinen Teleportknoten über tsh +tshNode.displayName=Teleport-Knoten +tshNode.displayDescription=Verbindung zu einem Teleport-Knoten in einem Cluster +teleportCluster=Cluster +teleportClusterDescription=Der Cluster, in dem sich der Knoten befindet +teleportProxy=Proxy +teleportProxyDescription=Der Proxy-Server, der für die Verbindung mit dem Knoten verwendet wird +teleportHost=Host +teleportHostDescription=Der Hostname des Knotens +teleportUser=Benutzer +teleportUserDescription=Der Benutzer, der sich als +login=Anmeldung diff --git a/lang/proc/strings/translations_en.properties b/lang/proc/strings/translations_en.properties index 8d9905f74..2bd4e9428 100644 --- a/lang/proc/strings/translations_en.properties +++ b/lang/proc/strings/translations_en.properties @@ -334,4 +334,34 @@ dockerContextActions=Context actions k8sPodActions=Pod actions openVnc=Set up VNC commandGroup.displayName=Command group -commandGroup.displayDescription=Group available commands for a system \ No newline at end of file +commandGroup.displayDescription=Group available commands for a system +serial.displayName=Serial connection +serial.displayDescription=Open a serial connection in a terminal +serialPort=Serial port +serialPortDescription=The serial port / device to connect to +baudRate=Baud rate +dataBits=Data bits +stopBits=Stop bits +parity=Parity +flowControlWindow=Flow control +serialImplementation=Serial implementation +serialImplementationDescription=The tool to use to connect to the serial port +serialHost=Host +serialHostDescription=The system to access the serial port on +serialPortConfiguration=Serial port configuration +serialPortConfigurationDescription=Configuration parameters of the connected serial device +serialInformation=Serial information +openXShell=Open in XShell +tsh.displayName=Teleport +tsh.displayDescription=Connect to your teleport nodes via tsh +tshNode.displayName=Teleport node +tshNode.displayDescription=Connect to a teleport node in a cluster +teleportCluster=Cluster +teleportClusterDescription=The cluster the node is in +teleportProxy=Proxy +teleportProxyDescription=The proxy server used to connect to the node +teleportHost=Host +teleportHostDescription=The host name of the node +teleportUser=User +teleportUserDescription=The user to login as +login=Login diff --git a/lang/proc/strings/translations_es.properties b/lang/proc/strings/translations_es.properties index 83a12ac97..17ac8709f 100644 --- a/lang/proc/strings/translations_es.properties +++ b/lang/proc/strings/translations_es.properties @@ -333,3 +333,33 @@ k8sPodActions=Acciones del pod openVnc=Configurar VNC commandGroup.displayName=Grupo de comandos commandGroup.displayDescription=Agrupa los comandos disponibles para un sistema +serial.displayName=Conexión en serie +serial.displayDescription=Abrir una conexión serie en un terminal +serialPort=Puerto serie +serialPortDescription=El puerto serie / dispositivo al que conectarse +baudRate=Velocidad en baudios +dataBits=Bits de datos +stopBits=Bits de parada +parity=Paridad +flowControlWindow=Control de flujo +serialImplementation=Aplicación en serie +serialImplementationDescription=La herramienta que hay que utilizar para conectarse al puerto serie +serialHost=Anfitrión +serialHostDescription=El sistema para acceder al puerto serie en +serialPortConfiguration=Configuración del puerto serie +serialPortConfigurationDescription=Parámetros de configuración del dispositivo serie conectado +serialInformation=Información en serie +openXShell=Abrir en XShell +tsh.displayName=Teletransporte +tsh.displayDescription=Conéctate a tus nodos de teletransporte mediante tsh +tshNode.displayName=Nodo de teletransporte +tshNode.displayDescription=Conectarse a un nodo de teletransporte en un clúster +teleportCluster=Clúster +teleportClusterDescription=El clúster en el que está el nodo +teleportProxy=Proxy +teleportProxyDescription=El servidor proxy utilizado para conectarse al nodo +teleportHost=Anfitrión +teleportHostDescription=El nombre de host del nodo +teleportUser=Usuario +teleportUserDescription=El usuario con el que iniciar sesión +login=Inicio de sesión diff --git a/lang/proc/strings/translations_fr.properties b/lang/proc/strings/translations_fr.properties index 20b4cc865..d8b47b99b 100644 --- a/lang/proc/strings/translations_fr.properties +++ b/lang/proc/strings/translations_fr.properties @@ -333,3 +333,33 @@ k8sPodActions=Actions de pods openVnc=Configurer VNC commandGroup.displayName=Groupe de commande commandGroup.displayDescription=Groupe de commandes disponibles pour un système +serial.displayName=Connexion série +serial.displayDescription=Ouvrir une connexion série dans un terminal +serialPort=Port série +serialPortDescription=Le port série / le périphérique à connecter +baudRate=Taux de bauds +dataBits=Bits de données +stopBits=Bits d'arrêt +parity=Parité +flowControlWindow=Contrôle de flux +serialImplementation=Implémentation en série +serialImplementationDescription=L'outil à utiliser pour se connecter au port série +serialHost=Hôte +serialHostDescription=Le système pour accéder au port série sur +serialPortConfiguration=Configuration du port série +serialPortConfigurationDescription=Paramètres de configuration de l'appareil en série connecté +serialInformation=Informations en série +openXShell=Ouvrir dans XShell +tsh.displayName=Téléportation +tsh.displayDescription=Connecte-toi à tes nœuds de téléportation via tsh +tshNode.displayName=Nœud de téléportation +tshNode.displayDescription=Se connecter à un nœud de téléportation dans une grappe +teleportCluster=Groupe de travail +teleportClusterDescription=La grappe dans laquelle se trouve le nœud +teleportProxy=Proxy +teleportProxyDescription=Le serveur proxy utilisé pour se connecter au nœud +teleportHost=Hôte +teleportHostDescription=Le nom d'hôte du nœud +teleportUser=Utilisateur +teleportUserDescription=L'utilisateur à connecter en tant que +login=Connexion diff --git a/lang/proc/strings/translations_it.properties b/lang/proc/strings/translations_it.properties index dbef50ac6..b4f310ccd 100644 --- a/lang/proc/strings/translations_it.properties +++ b/lang/proc/strings/translations_it.properties @@ -333,3 +333,33 @@ k8sPodActions=Azioni del pod openVnc=Configurare VNC commandGroup.displayName=Gruppo di comando commandGroup.displayDescription=Gruppo di comandi disponibili per un sistema +serial.displayName=Connessione seriale +serial.displayDescription=Aprire una connessione seriale in un terminale +serialPort=Porta seriale +serialPortDescription=La porta seriale/dispositivo a cui connettersi +baudRate=Velocità di trasmissione +dataBits=Bit di dati +stopBits=Bit di stop +parity=Parità +flowControlWindow=Controllo del flusso +serialImplementation=Implementazione seriale +serialImplementationDescription=Lo strumento da utilizzare per collegarsi alla porta seriale +serialHost=Ospite +serialHostDescription=Il sistema per accedere alla porta seriale su +serialPortConfiguration=Configurazione della porta seriale +serialPortConfigurationDescription=Parametri di configurazione del dispositivo seriale collegato +serialInformation=Informazioni di serie +openXShell=Apri in XShell +tsh.displayName=Teletrasporto +tsh.displayDescription=Connettiti ai tuoi nodi di teletrasporto via tsh +tshNode.displayName=Nodo di teletrasporto +tshNode.displayDescription=Connettersi a un nodo di teletrasporto in un cluster +teleportCluster=Cluster +teleportClusterDescription=Il cluster in cui si trova il nodo +teleportProxy=Proxy +teleportProxyDescription=Il server proxy utilizzato per connettersi al nodo +teleportHost=Ospite +teleportHostDescription=Il nome host del nodo +teleportUser=Utente +teleportUserDescription=L'utente con cui effettuare il login +login=Accesso diff --git a/lang/proc/strings/translations_ja.properties b/lang/proc/strings/translations_ja.properties index 79c161735..2e24a3cd6 100644 --- a/lang/proc/strings/translations_ja.properties +++ b/lang/proc/strings/translations_ja.properties @@ -333,3 +333,33 @@ k8sPodActions=ポッドアクション openVnc=VNCを設定する commandGroup.displayName=コマンドグループ commandGroup.displayDescription=システムで使用可能なコマンドをグループ化する +serial.displayName=シリアル接続 +serial.displayDescription=ターミナルでシリアル接続を開く +serialPort=シリアルポート +serialPortDescription=接続するシリアルポート/デバイス +baudRate=ボーレート +dataBits=データビット +stopBits=ストップビット +parity=パリティ +flowControlWindow=フロー制御 +serialImplementation=シリアルの実装 +serialImplementationDescription=シリアルポートに接続するために使用するツール +serialHost=ホスト +serialHostDescription=のシリアルポートにアクセスするシステム +serialPortConfiguration=シリアルポートの設定 +serialPortConfigurationDescription=接続されたシリアル・デバイスの設定パラメーター +serialInformation=シリアル情報 +openXShell=XShellで開く +tsh.displayName=テレポート +tsh.displayDescription=tsh経由でテレポートノードに接続する +tshNode.displayName=テレポートノード +tshNode.displayDescription=クラスタ内のテレポートノードに接続する +teleportCluster=クラスター +teleportClusterDescription=ノードが属するクラスタ +teleportProxy=プロキシ +teleportProxyDescription=ノードへの接続に使用されるプロキシサーバー +teleportHost=ホスト +teleportHostDescription=ノードのホスト名 +teleportUser=ユーザー +teleportUserDescription=ログインするユーザー +login=ログイン diff --git a/lang/proc/strings/translations_nl.properties b/lang/proc/strings/translations_nl.properties index 7ea31c90b..b139a7bac 100644 --- a/lang/proc/strings/translations_nl.properties +++ b/lang/proc/strings/translations_nl.properties @@ -333,3 +333,33 @@ k8sPodActions=Pod acties openVnc=VNC instellen commandGroup.displayName=Opdrachtgroep commandGroup.displayDescription=Groep beschikbare commando's voor een systeem +serial.displayName=Seriële verbinding +serial.displayDescription=Een seriële verbinding in een terminal openen +serialPort=Seriële poort +serialPortDescription=De seriële poort / het apparaat waarmee verbinding moet worden gemaakt +baudRate=Baudrate +dataBits=Gegevensbits +stopBits=Stopbits +parity=Pariteit +flowControlWindow=Debietregeling +serialImplementation=Seriële implementatie +serialImplementationDescription=Het gereedschap om verbinding te maken met de seriële poort +serialHost=Host +serialHostDescription=Het systeem om toegang te krijgen tot de seriële poort op +serialPortConfiguration=Seriële poort configuratie +serialPortConfigurationDescription=Configuratieparameters van het aangesloten seriële apparaat +serialInformation=Seriële informatie +openXShell=Openen in XShell +tsh.displayName=Teleport +tsh.displayDescription=Verbinding maken met je teleportknooppunten via tsh +tshNode.displayName=Teleport knooppunt +tshNode.displayDescription=Verbinding maken met een teleportknooppunt in een cluster +teleportCluster=Cluster +teleportClusterDescription=Het cluster waar het knooppunt zich in bevindt +teleportProxy=Proxy +teleportProxyDescription=De proxyserver die wordt gebruikt om verbinding te maken met het knooppunt +teleportHost=Host +teleportHostDescription=De hostnaam van het knooppunt +teleportUser=Gebruiker +teleportUserDescription=De gebruiker om als in te loggen +login=Inloggen diff --git a/lang/proc/strings/translations_pt.properties b/lang/proc/strings/translations_pt.properties index 2d495a348..0b49ec78e 100644 --- a/lang/proc/strings/translations_pt.properties +++ b/lang/proc/strings/translations_pt.properties @@ -333,3 +333,33 @@ k8sPodActions=Acções de pod openVnc=Configura o VNC commandGroup.displayName=Grupo de comandos commandGroup.displayDescription=Agrupa os comandos disponíveis para um sistema +serial.displayName=Ligação em série +serial.displayDescription=Abre uma ligação de série num terminal +serialPort=Porta de série +serialPortDescription=A porta de série / dispositivo a ligar +baudRate=Taxa de transmissão +dataBits=Bits de dados +stopBits=Bits de paragem +parity=Paridade +flowControlWindow=Controlo de fluxo +serialImplementation=Implementação em série +serialImplementationDescription=A ferramenta a utilizar para ligar à porta série +serialHost=Apresenta +serialHostDescription=O sistema para aceder à porta série em +serialPortConfiguration=Configuração da porta de série +serialPortConfigurationDescription=Parâmetros de configuração do dispositivo de série ligado +serialInformation=Informação de série +openXShell=Abre no XShell +tsh.displayName=Teletransporte +tsh.displayDescription=Liga-te aos teus nós de teletransporte via tsh +tshNode.displayName=Nó de teletransporte +tshNode.displayDescription=Liga-te a um nó de teletransporte num cluster +teleportCluster=Agrupa +teleportClusterDescription=O cluster em que o nó se encontra +teleportProxy=Proxy +teleportProxyDescription=O servidor proxy utilizado para ligar ao nó +teleportHost=Apresenta +teleportHostDescription=O nome do anfitrião do nó +teleportUser=Utilizador +teleportUserDescription=O utilizador para iniciar sessão como +login=Acede diff --git a/lang/proc/strings/translations_ru.properties b/lang/proc/strings/translations_ru.properties index 9b528e587..dd988f7d4 100644 --- a/lang/proc/strings/translations_ru.properties +++ b/lang/proc/strings/translations_ru.properties @@ -333,3 +333,33 @@ k8sPodActions=Действия в капсуле openVnc=Настройте VNC commandGroup.displayName=Группа команд commandGroup.displayDescription=Группа доступных команд для системы +serial.displayName=Последовательное соединение +serial.displayDescription=Открыть последовательное соединение в терминале +serialPort=Последовательный порт +serialPortDescription=Последовательный порт/устройство, к которому нужно подключиться +baudRate=Скорость передачи данных +dataBits=Биты данных +stopBits=Стоп-биты +parity=Четность +flowControlWindow=Контроль потока +serialImplementation=Последовательная реализация +serialImplementationDescription=Инструмент, который нужно использовать для подключения к последовательному порту +serialHost=Хост +serialHostDescription=Система для доступа к последовательному порту на +serialPortConfiguration=Конфигурация последовательного порта +serialPortConfigurationDescription=Параметры конфигурации подключенного последовательного устройства +serialInformation=Серийная информация +openXShell=Открыть в XShell +tsh.displayName=Телепорт +tsh.displayDescription=Подключайтесь к узлам телепортации через tsh +tshNode.displayName=Узел телепортации +tshNode.displayDescription=Подключение к узлу телепортации в кластере +teleportCluster=Кластер +teleportClusterDescription=Кластер, в котором находится узел +teleportProxy=Прокси +teleportProxyDescription=Прокси-сервер, используемый для подключения к узлу +teleportHost=Хост +teleportHostDescription=Имя хоста узла +teleportUser=Пользователь +teleportUserDescription=Пользователь, от имени которого нужно войти в систему +login=Логин diff --git a/lang/proc/strings/translations_tr.properties b/lang/proc/strings/translations_tr.properties index ce0890001..47a971d7d 100644 --- a/lang/proc/strings/translations_tr.properties +++ b/lang/proc/strings/translations_tr.properties @@ -333,3 +333,33 @@ k8sPodActions=Pod eylemleri openVnc=VNC'yi ayarlama commandGroup.displayName=Komuta grubu commandGroup.displayDescription=Bir sistem için mevcut komutları gruplama +serial.displayName=Seri bağlantı +serial.displayDescription=Terminalde bir seri bağlantı açın +serialPort=Seri bağlantı noktası +serialPortDescription=Bağlanılacak seri port / cihaz +baudRate=Baud hızı +dataBits=Veri bitleri +stopBits=Dur bitleri +parity=Parite +flowControlWindow=Akış kontrolü +serialImplementation=Seri uygulama +serialImplementationDescription=Seri porta bağlanmak için kullanılacak araç +serialHost=Ev sahibi +serialHostDescription=Seri porta erişmek için sistem +serialPortConfiguration=Seri bağlantı noktası yapılandırması +serialPortConfigurationDescription=Bağlı seri cihazın konfigürasyon parametreleri +serialInformation=Seri bilgileri +openXShell=XShell'de Aç +tsh.displayName=Işınlanma +tsh.displayDescription=Teleport düğümlerinize tsh ile bağlanın +tshNode.displayName=Işınlanma düğümü +tshNode.displayDescription=Kümedeki bir ışınlanma düğümüne bağlanma +teleportCluster=Küme +teleportClusterDescription=Düğümün içinde bulunduğu küme +teleportProxy=Proxy +teleportProxyDescription=Düğüme bağlanmak için kullanılan proxy sunucusu +teleportHost=Ev sahibi +teleportHostDescription=Düğümün ana bilgisayar adı +teleportUser=Kullanıcı +teleportUserDescription=Giriş yapılacak kullanıcı +login=Giriş diff --git a/lang/proc/strings/translations_zh.properties b/lang/proc/strings/translations_zh.properties index 36ba085f9..e1c69db04 100644 --- a/lang/proc/strings/translations_zh.properties +++ b/lang/proc/strings/translations_zh.properties @@ -333,3 +333,33 @@ k8sPodActions=Pod 操作 openVnc=设置 VNC commandGroup.displayName=命令组 commandGroup.displayDescription=系统可用命令组 +serial.displayName=串行连接 +serial.displayDescription=在终端中打开串行连接 +serialPort=串行端口 +serialPortDescription=要连接的串行端口/设备 +baudRate=波特率 +dataBits=数据位 +stopBits=停止位 +parity=奇偶校验 +flowControlWindow=流量控制 +serialImplementation=串行实施 +serialImplementationDescription=用于连接串行端口的工具 +serialHost=主机 +serialHostDescription=访问串行端口的系统 +serialPortConfiguration=串行端口配置 +serialPortConfigurationDescription=所连接串行设备的配置参数 +serialInformation=序列信息 +openXShell=在 XShell 中打开 +tsh.displayName=远程传输 +tsh.displayDescription=通过 tsh 连接到远程传送节点 +tshNode.displayName=远距传送节点 +tshNode.displayDescription=连接到群集中的远程传送节点 +teleportCluster=群组 +teleportClusterDescription=节点所在的集群 +teleportProxy=代理 +teleportProxyDescription=用于连接节点的代理服务器 +teleportHost=主机 +teleportHostDescription=节点的主机名 +teleportUser=用户 +teleportUserDescription=要登录的用户 +login=登录 diff --git a/lang/proc/texts/serialImplementation_da.md b/lang/proc/texts/serialImplementation_da.md new file mode 100644 index 000000000..196b1b48e --- /dev/null +++ b/lang/proc/texts/serialImplementation_da.md @@ -0,0 +1,10 @@ +# Implementeringer + +XPipe uddelegerer den serielle håndtering til eksterne værktøjer. +Der er flere tilgængelige værktøjer, som XPipe kan uddelegere til, hver med deres egne fordele og ulemper. +For at bruge dem kræves det, at de er tilgængelige på værtssystemet. +De fleste muligheder burde være understøttet af alle værktøjer, men nogle mere eksotiske muligheder er det måske ikke. + +Før der oprettes forbindelse, kontrollerer XPipe, at det valgte værktøj er installeret og understøtter alle konfigurerede muligheder. +Hvis denne kontrol er vellykket, starter det valgte værktøj. + diff --git a/lang/proc/texts/serialImplementation_de.md b/lang/proc/texts/serialImplementation_de.md new file mode 100644 index 000000000..8ac511a41 --- /dev/null +++ b/lang/proc/texts/serialImplementation_de.md @@ -0,0 +1,10 @@ +# Implementierungen + +XPipe delegiert die serielle Verarbeitung an externe Tools. +Es gibt mehrere Tools, an die XPipe delegieren kann, jedes mit seinen eigenen Vor- und Nachteilen. +Um sie zu nutzen, müssen sie auf dem Hostsystem verfügbar sein. +Die meisten Optionen sollten von allen Tools unterstützt werden, aber einige exotischere Optionen sind es vielleicht nicht. + +Bevor eine Verbindung hergestellt wird, prüft XPipe, ob das ausgewählte Tool installiert ist und alle konfigurierten Optionen unterstützt. +Wenn diese Prüfung erfolgreich ist, wird das ausgewählte Tool gestartet. + diff --git a/lang/proc/texts/serialImplementation_en.md b/lang/proc/texts/serialImplementation_en.md new file mode 100644 index 000000000..834da94a8 --- /dev/null +++ b/lang/proc/texts/serialImplementation_en.md @@ -0,0 +1,10 @@ +# Implementations + +XPipe delegates the serial handling to external tools. +There are multiple available tools XPipe can delegate to, each with their own advantages and disadvantages. +To use them, it is required that they are available on the host system. +Most options should be supported by all tools, but some more exotic options might not be. + +Before connecting, XPipe will verify that the selected tool is installed and supports all configured options. +If that check is successful, the selected tool will launch. + diff --git a/lang/proc/texts/serialImplementation_es.md b/lang/proc/texts/serialImplementation_es.md new file mode 100644 index 000000000..6c99e0bfa --- /dev/null +++ b/lang/proc/texts/serialImplementation_es.md @@ -0,0 +1,10 @@ +# Implementaciones + +XPipe delega el manejo de la serie en herramientas externas. +Hay múltiples herramientas disponibles en las que XPipe puede delegar, cada una con sus propias ventajas e inconvenientes. +Para utilizarlas, es necesario que estén disponibles en el sistema anfitrión. +La mayoría de las opciones deberían estar soportadas por todas las herramientas, pero algunas opciones más exóticas podrían no estarlo. + +Antes de conectarse, XPipe comprobará que la herramienta seleccionada está instalada y admite todas las opciones configuradas. +Si la comprobación es correcta, se iniciará la herramienta seleccionada. + diff --git a/lang/proc/texts/serialImplementation_fr.md b/lang/proc/texts/serialImplementation_fr.md new file mode 100644 index 000000000..a9bacebee --- /dev/null +++ b/lang/proc/texts/serialImplementation_fr.md @@ -0,0 +1,10 @@ +# Implantations + +XPipe délègue la gestion de la série à des outils externes. +Il existe plusieurs outils disponibles auxquels XPipe peut déléguer, chacun ayant ses propres avantages et inconvénients. +Pour les utiliser, il faut qu'ils soient disponibles sur le système hôte. +La plupart des options devraient être supportées par tous les outils, mais certaines options plus exotiques pourraient ne pas l'être. + +Avant de se connecter, XPipe vérifie que l'outil sélectionné est installé et qu'il prend en charge toutes les options configurées. +Si cette vérification est concluante, l'outil sélectionné sera lancé. + diff --git a/lang/proc/texts/serialImplementation_it.md b/lang/proc/texts/serialImplementation_it.md new file mode 100644 index 000000000..3ec0d8bf8 --- /dev/null +++ b/lang/proc/texts/serialImplementation_it.md @@ -0,0 +1,10 @@ +# Implementazioni + +XPipe delega la gestione della serialità a strumenti esterni. +Esistono diversi strumenti a cui XPipe può delegare, ognuno con i propri vantaggi e svantaggi. +Per poterli utilizzare, è necessario che siano disponibili sul sistema host. +La maggior parte delle opzioni dovrebbe essere supportata da tutti gli strumenti, ma alcune opzioni più esotiche potrebbero non esserlo. + +Prima di connettersi, XPipe verifica che lo strumento selezionato sia installato e che supporti tutte le opzioni configurate. +Se la verifica ha esito positivo, lo strumento selezionato viene avviato. + diff --git a/lang/proc/texts/serialImplementation_ja.md b/lang/proc/texts/serialImplementation_ja.md new file mode 100644 index 000000000..d6fe6e650 --- /dev/null +++ b/lang/proc/texts/serialImplementation_ja.md @@ -0,0 +1,10 @@ +# 実装 + +XPipeはシリアル処理を外部ツールに委譲する。 +XPipeが委譲できるツールは複数あり、それぞれに長所と短所がある。 +それらを使用するには、ホストシステム上で使用可能であることが必要である。 +ほとんどのオプションはすべてのツールでサポートされているはずだが、よりエキゾチックなオプションはサポートされていないかもしれない。 + +接続する前に、XPipeは、選択したツールがインストールされ、設定されたすべてのオプションに対応しているかどうかを確認する。 +チェックが成功すると、選択したツールが起動する。 + diff --git a/lang/proc/texts/serialImplementation_nl.md b/lang/proc/texts/serialImplementation_nl.md new file mode 100644 index 000000000..9c9d5ada5 --- /dev/null +++ b/lang/proc/texts/serialImplementation_nl.md @@ -0,0 +1,10 @@ +# Implementaties + +XPipe delegeert de seriële afhandeling naar externe tools. +Er zijn meerdere beschikbare tools waaraan XPipe kan delegeren, elk met hun eigen voor- en nadelen. +Om ze te gebruiken is het vereist dat ze beschikbaar zijn op het hostsysteem. +De meeste opties zouden door alle gereedschappen ondersteund moeten worden, maar sommige meer exotische opties misschien niet. + +Voordat er verbinding wordt gemaakt, controleert XPipe of het geselecteerde gereedschap is geïnstalleerd en alle geconfigureerde opties ondersteunt. +Als die controle succesvol is, wordt het geselecteerde gereedschap gestart. + diff --git a/lang/proc/texts/serialImplementation_pt.md b/lang/proc/texts/serialImplementation_pt.md new file mode 100644 index 000000000..2b665a6cc --- /dev/null +++ b/lang/proc/texts/serialImplementation_pt.md @@ -0,0 +1,10 @@ +# Implementações + +XPipe delega o manuseio serial para ferramentas externas. +Existem várias ferramentas disponíveis para as quais o XPipe pode delegar, cada uma com suas próprias vantagens e desvantagens. +Para as utilizar, é necessário que estejam disponíveis no sistema anfitrião. +A maioria das opções deve ser suportada por todas as ferramentas, mas algumas opções mais exóticas podem não ser. + +Antes de ligar, o XPipe verifica se a ferramenta selecionada está instalada e se suporta todas as opções configuradas. +Se essa verificação for bem sucedida, a ferramenta selecionada será iniciada. + diff --git a/lang/proc/texts/serialImplementation_ru.md b/lang/proc/texts/serialImplementation_ru.md new file mode 100644 index 000000000..4af78f64f --- /dev/null +++ b/lang/proc/texts/serialImplementation_ru.md @@ -0,0 +1,10 @@ +# Реализации + +XPipe делегирует работу с последовательным интерфейсом внешним инструментам. +Существует несколько доступных инструментов, которым XPipe может делегировать свои полномочия, каждый из которых имеет свои преимущества и недостатки. +Чтобы использовать их, необходимо, чтобы они были доступны в хост-системе. +Большинство опций должны поддерживаться всеми инструментами, но некоторые более экзотические опции могут не поддерживаться. + +Перед подключением XPipe проверит, что выбранный инструмент установлен и поддерживает все настроенные опции. +Если проверка прошла успешно, выбранный инструмент будет запущен. + diff --git a/lang/proc/texts/serialImplementation_tr.md b/lang/proc/texts/serialImplementation_tr.md new file mode 100644 index 000000000..60a7f6bd0 --- /dev/null +++ b/lang/proc/texts/serialImplementation_tr.md @@ -0,0 +1,10 @@ +# Uygulamalar + +XPipe seri işlemeyi harici araçlara devreder. +XPipe'ın temsilci olarak kullanabileceği, her biri kendi avantaj ve dezavantajlarına sahip birden fazla araç mevcuttur. +Bunları kullanmak için, ana sistemde mevcut olmaları gerekir. +Çoğu seçenek tüm araçlar tarafından desteklenmelidir, ancak bazı daha egzotik seçenekler desteklenmeyebilir. + +XPipe bağlanmadan önce seçilen aracın yüklü olduğunu ve yapılandırılan tüm seçenekleri desteklediğini doğrular. +Bu kontrol başarılı olursa, seçilen araç başlatılır. + diff --git a/lang/proc/texts/serialImplementation_zh.md b/lang/proc/texts/serialImplementation_zh.md new file mode 100644 index 000000000..d04386559 --- /dev/null +++ b/lang/proc/texts/serialImplementation_zh.md @@ -0,0 +1,10 @@ +# 实现 + +XPipe将串行处理委托给外部工具。 +XPipe 可以委托多种可用工具,每种工具都有自己的优缺点。 +要使用这些工具,主机系统上必须有这些工具。 +大多数选项应为所有工具所支持,但一些较为特殊的选项可能不支持。 + +在连接之前,XPipe 将验证所选工具是否已安装并支持所有配置选项。 +如果检查成功,所选工具将启动。 + diff --git a/lang/proc/texts/serialPort_da.md b/lang/proc/texts/serialPort_da.md new file mode 100644 index 000000000..a4fc65eff --- /dev/null +++ b/lang/proc/texts/serialPort_da.md @@ -0,0 +1,20 @@ +## Windows + +På Windows-systemer henviser man typisk til serielle porte via `COM`. +XPipe understøtter også blot at angive indekset uden præfikset `COM`. +For at adressere porte større end 9 skal du bruge UNC-stiformen med `\\.\COM`. + +Hvis du har installeret en WSL1-distribution, kan du også henvise til de serielle porte inde fra WSL-distributionen via `/dev/ttyS`. +Dette virker dog ikke længere med WSL2. +Hvis du har et WSL1-system, kan du bruge det som vært for den serielle forbindelse og bruge tty-notationen til at få adgang til det med XPipe. + +## Linux + +På Linux-systemer kan du typisk få adgang til de serielle porte via `/dev/ttyS`. +Hvis du kender ID'et på den tilsluttede enhed, men ikke ønsker at holde styr på den serielle port, kan du også henvise til dem via `/dev/serial/by-id/`. +Du kan få en liste over alle tilgængelige serielle porte med deres ID'er ved at køre `ls /dev/serial/by-id/*`. + +## macOS + +På macOS kan de serielle portnavne være stort set hvad som helst, men har normalt formen `/dev/tty.`, hvor id er den interne enhedsidentifikator. +Ved at køre `ls /dev/tty.*` kan man finde tilgængelige serielle porte. diff --git a/lang/proc/texts/serialPort_de.md b/lang/proc/texts/serialPort_de.md new file mode 100644 index 000000000..ff0ad15c7 --- /dev/null +++ b/lang/proc/texts/serialPort_de.md @@ -0,0 +1,20 @@ +## Windows + +Auf Windows-Systemen bezeichnest du serielle Schnittstellen normalerweise mit `COM`. +XPipe unterstützt auch die bloße Angabe des Index ohne das Präfix `COM`. +Um Ports größer als 9 anzusprechen, musst du die UNC-Pfadform mit `\.\COM` verwenden. + +Wenn du eine WSL1-Distribution installiert hast, kannst du die seriellen Schnittstellen auch aus der WSL-Distribution heraus über `/dev/ttyS` ansprechen. +Das funktioniert allerdings nicht mehr mit WSL2. +Wenn du ein WSL1-System hast, kannst du dieses als Host für diese serielle Verbindung verwenden und die tty-Notation nutzen, um mit XPipe darauf zuzugreifen. + +## Linux + +Auf Linux-Systemen kannst du normalerweise über `/dev/ttyS` auf die seriellen Schnittstellen zugreifen. +Wenn du die ID des angeschlossenen Geräts kennst, dir aber die serielle Schnittstelle nicht merken willst, kannst du sie auch über `/dev/serial/by-id/` ansprechen. +Du kannst alle verfügbaren seriellen Schnittstellen mit ihren IDs auflisten, indem du `ls /dev/serial/by-id/*` ausführst. + +## macOS + +Unter macOS können die Namen der seriellen Schnittstellen so ziemlich alles sein, aber normalerweise haben sie die Form `/dev/tty.`, wobei id die interne Gerätekennung ist. +Wenn du `ls /dev/tty.*` ausführst, solltest du die verfügbaren seriellen Schnittstellen finden. diff --git a/lang/proc/texts/serialPort_en.md b/lang/proc/texts/serialPort_en.md new file mode 100644 index 000000000..ee9080503 --- /dev/null +++ b/lang/proc/texts/serialPort_en.md @@ -0,0 +1,20 @@ +## Windows + +On Windows systems you typically refer to serial ports via `COM`. +XPipe also supports just specifying the index without the `COM` prefix. +To address ports greater than 9, you have to use the UNC path form with `\\.\COM`. + +If you have a WSL1 distribution installed, you can also reference the serial ports from within the WSL distribution via `/dev/ttyS`. +This it does not work with WSL2 anymore though. +If you have a WSL1 system, you can use this one as the host for this serial connection and use the tty notation to access it with XPipe. + +## Linux + +On Linux systems you can typically access the serial ports via `/dev/ttyS`. +If you know the ID of the connected device but don't want to keep track of the serial port, you can also reference them via `/dev/serial/by-id/`. +You can list all available serial ports with their IDs by running `ls /dev/serial/by-id/*`. + +## macOS + +On macOS, the serial port names can be pretty much anything, but usually have the form of `/dev/tty.` where the id the internal device identifier. +Running `ls /dev/tty.*` should find available serial ports. diff --git a/lang/proc/texts/serialPort_es.md b/lang/proc/texts/serialPort_es.md new file mode 100644 index 000000000..9ce1d1ef0 --- /dev/null +++ b/lang/proc/texts/serialPort_es.md @@ -0,0 +1,20 @@ +## Windows + +En los sistemas Windows sueles referirte a los puertos serie mediante `COM`. +XPipe también admite sólo especificar el índice sin el prefijo `COM`. +Para dirigirte a puertos mayores de 9, tienes que utilizar la forma de ruta UNC con `COM`. + +Si tienes instalada una distribución WSL1, también puedes hacer referencia a los puertos serie desde dentro de la distribución WSL mediante `/dev/ttyS`. +Sin embargo, esto ya no funciona con WSL2. +Si tienes un sistema WSL1, puedes utilizarlo como host para esta conexión serie y utilizar la notación tty para acceder a él con XPipe. + +## Linux + +En los sistemas Linux normalmente puedes acceder a los puertos serie a través de `/dev/ttyS`. +Si conoces el ID del dispositivo conectado pero no quieres seguir la pista del puerto serie, también puedes referenciarlos mediante `/dev/serial/by-id/`. +Puedes listar todos los puertos serie disponibles con sus ID ejecutando `ls /dev/serial/by-id/*`. + +## macOS + +En macOS, los nombres de los puertos serie pueden ser prácticamente cualquier cosa, pero suelen tener la forma de `/dev/tty.` donde id es el identificador interno del dispositivo. +Ejecutando `ls /dev/tty.*` deberías encontrar los puertos serie disponibles. diff --git a/lang/proc/texts/serialPort_fr.md b/lang/proc/texts/serialPort_fr.md new file mode 100644 index 000000000..666f490e1 --- /dev/null +++ b/lang/proc/texts/serialPort_fr.md @@ -0,0 +1,20 @@ +## Windows + +Sur les systèmes Windows, tu fais généralement référence aux ports série via `COM`. +XPipe prend également en charge la spécification de l'index sans le préfixe `COM`. +Pour adresser des ports supérieurs à 9, il faut utiliser la forme de chemin UNC avec `\\N-COM`. + +Si tu as installé une distribution WSL1, tu peux aussi référencer les ports série à partir de la distribution WSL via `/dev/ttyS`. +Cela ne fonctionne plus avec WSL2. +Si tu as un système WSL1, tu peux utiliser celui-ci comme hôte pour cette connexion série et utiliser la notation tty pour y accéder avec XPipe. + +## Linux + +Sur les systèmes Linux, tu peux généralement accéder aux ports série via `/dev/ttyS`. +Si tu connais l'ID de l'appareil connecté mais que tu ne veux pas garder trace du port série, tu peux aussi les référencer via `/dev/serial/by-id/`. +Tu peux dresser la liste de tous les ports série disponibles avec leur ID en exécutant `ls /dev/serial/by-id/*`. + +## macOS + +Sur macOS, les noms des ports série peuvent être à peu près n'importe quoi, mais ils ont généralement la forme `/dev/tty.` où l'id l'identifiant interne du périphérique. +L'exécution de `ls /dev/tty.*` devrait permettre de trouver les ports série disponibles. diff --git a/lang/proc/texts/serialPort_it.md b/lang/proc/texts/serialPort_it.md new file mode 100644 index 000000000..c38e01bf6 --- /dev/null +++ b/lang/proc/texts/serialPort_it.md @@ -0,0 +1,20 @@ +## Windows + +Nei sistemi Windows di solito ci si riferisce alle porte seriali tramite `COM`. +XPipe supporta anche la semplice indicazione dell'indice senza il prefisso `COM`. +Per indirizzare le porte superiori a 9, devi utilizzare la forma UNC path con `\\.\COM`. + +Se hai installato una distribuzione WSL1, puoi anche fare riferimento alle porte seriali dall'interno della distribuzione WSL tramite `/dev/ttyS`. +Questo però non funziona più con WSL2. +Se hai un sistema WSL1, puoi usarlo come host per questa connessione seriale e utilizzare la notazione tty per accedervi con XPipe. + +## Linux + +Sui sistemi Linux puoi accedere alle porte seriali tramite `/dev/ttyS`. +Se conosci l'ID del dispositivo collegato ma non vuoi tenere traccia della porta seriale, puoi anche fare riferimento ad esso tramite `/dev/serial/by-id/`. +Puoi elencare tutte le porte seriali disponibili con i relativi ID eseguendo `ls /dev/serial/by-id/*`. + +## macOS + +Su macOS, i nomi delle porte seriali possono essere praticamente qualsiasi cosa, ma di solito hanno la forma di `/dev/tty.` dove l'id è l'identificatore interno del dispositivo. +L'esecuzione di `ls /dev/tty.*` dovrebbe trovare le porte seriali disponibili. diff --git a/lang/proc/texts/serialPort_ja.md b/lang/proc/texts/serialPort_ja.md new file mode 100644 index 000000000..1b601ade8 --- /dev/null +++ b/lang/proc/texts/serialPort_ja.md @@ -0,0 +1,20 @@ +## ウィンドウズ + +Windowsシステムでは、通常`COM`でシリアルポートを参照する。 +XPipeでは、`COM`という接頭辞なしでインデックスを指定することもできる。 +9以上のポートを指定するには、`COM`でUNCパス形式を使わなければならない。 + +WSL1ディストリビューションがインストールされている場合、WSLディストリビューション内から`/dev/ttyS`でシリアルポートを参照することもできる。 +しかし、これはWSL2では動作しない。 +WSL1システムを持っている場合は、このシステムをシリアル接続のホストとして使用し、XPipeでアクセスするためにtty記法を使用することができる。 + +## Linux + +Linuxシステムでは、通常`/dev/ttyS`経由でシリアルポートにアクセスできる。 +接続されているデバイスのIDは知っているが、シリアルポートを追跡したくない場合は、`/dev/serial/by-id/<デバイスID>`で参照することもできる。 +`ls /dev/serial/by-id/*`を実行すれば、利用可能なすべてのシリアルポートをID付きで一覧できる。 + +## macOS + +macOSでは、シリアルポート名はほとんど何でも良いが、通常は`/dev/tty.`の形をしており、idは内部デバイス識別子である。 +`ls /dev/tty.*`を実行すると、利用可能なシリアルポートが見つかるはずである。 diff --git a/lang/proc/texts/serialPort_nl.md b/lang/proc/texts/serialPort_nl.md new file mode 100644 index 000000000..6c906c4b9 --- /dev/null +++ b/lang/proc/texts/serialPort_nl.md @@ -0,0 +1,20 @@ +## Windows + +Op Windows systemen verwijs je meestal naar seriële poorten via `COM`. +XPipe ondersteunt ook het opgeven van de index zonder het `COM` voorvoegsel. +Om poorten groter dan 9 te adresseren, moet je de UNC pad vorm gebruiken met `\.\COM`. + +Als je een WSL1 distributie hebt geïnstalleerd, kun je de seriële poorten ook vanuit de WSL distributie benaderen via `/dev/ttyS`. +Dit werkt echter niet meer met WSL2. +Als je een WSL1 systeem hebt, kun je deze gebruiken als host voor deze seriële verbinding en de tty notatie gebruiken om deze te benaderen met XPipe. + +## Linux + +Op Linux systemen heb je meestal toegang tot de seriële poorten via `/dev/ttyS`. +Als je de ID van het aangesloten apparaat weet, maar de seriële poort niet wilt bijhouden, kun je ze ook benaderen via `/dev/serial/by-id/`. +Je kunt een lijst maken van alle beschikbare seriële poorten met hun ID's door `ls /dev/serial/by-id/*` uit te voeren. + +## macOS + +Op macOS kunnen de namen van de seriële poorten van alles zijn, maar meestal hebben ze de vorm `/dev/tty.` waarbij de id de interne apparaat-ID is. +Het uitvoeren van `ls /dev/tty.*` zou beschikbare seriële poorten moeten vinden. diff --git a/lang/proc/texts/serialPort_pt.md b/lang/proc/texts/serialPort_pt.md new file mode 100644 index 000000000..9a5873c86 --- /dev/null +++ b/lang/proc/texts/serialPort_pt.md @@ -0,0 +1,20 @@ +## Windows + +Nos sistemas Windows, normalmente referes-te às portas série através de `COM`. +O XPipe também suporta apenas a especificação do índice sem o prefixo `COM`. +Para endereçar portas maiores que 9, é necessário usar a forma de caminho UNC com `\\.\COM`. + +Se tiver uma distribuição WSL1 instalada, também pode referenciar as portas seriais de dentro da distribuição WSL via `/dev/ttyS`. +No entanto, isso não funciona mais com o WSL2. +Se tiveres um sistema WSL1, podes usar este como anfitrião para esta ligação série e usar a notação tty para aceder a ele com o XPipe. + +## Linux + +Em sistemas Linux podes tipicamente aceder às portas série via `/dev/ttyS`. +Se souberes o ID do dispositivo ligado mas não quiseres manter o registo da porta série, podes também referenciá-los através de `/dev/serial/by-id/`. +Podes listar todas as portas série disponíveis com os seus IDs ao correr `ls /dev/serial/by-id/*`. + +## macOS + +No macOS, os nomes das portas seriais podem ser praticamente qualquer coisa, mas geralmente têm a forma de `/dev/tty.` onde o id é o identificador interno do dispositivo. +Executar `ls /dev/tty.*` deve encontrar portas seriais disponíveis. diff --git a/lang/proc/texts/serialPort_ru.md b/lang/proc/texts/serialPort_ru.md new file mode 100644 index 000000000..53c287baf --- /dev/null +++ b/lang/proc/texts/serialPort_ru.md @@ -0,0 +1,20 @@ +## Windows + +В системах Windows ты обычно обращаешься к последовательным портам через `COM`. +XPipe также поддерживает простое указание индекса без префикса `COM`. +Чтобы обратиться к портам больше 9, тебе придется использовать форму UNC-пути с `\\\.\COM`. + +Если у тебя установлен дистрибутив WSL1, ты также можешь обращаться к последовательным портам из дистрибутива WSL через `/dev/ttyS`. +Однако с WSL2 это уже не работает. +Если у тебя есть система WSL1, ты можешь использовать ее в качестве хоста для этого последовательного соединения и использовать нотацию tty для доступа к ней с помощью XPipe. + +## Linux + +В Linux-системах ты обычно можешь получить доступ к последовательным портам через `/dev/ttyS`. +Если ты знаешь ID подключенного устройства, но не хочешь следить за последовательным портом, ты также можешь обратиться к ним через `/dev/serial/by-id/`. +Ты можешь перечислить все доступные последовательные порты с их идентификаторами, выполнив команду `ls /dev/serial/by-id/*`. + +## macOS + +В macOS имена последовательных портов могут быть практически любыми, но обычно имеют вид `/dev/tty.`, где id - внутренний идентификатор устройства. +Запуск `ls /dev/tty.*` должен найти доступные последовательные порты. diff --git a/lang/proc/texts/serialPort_tr.md b/lang/proc/texts/serialPort_tr.md new file mode 100644 index 000000000..d9458b92e --- /dev/null +++ b/lang/proc/texts/serialPort_tr.md @@ -0,0 +1,20 @@ +## Windows + +Windows sistemlerinde seri portlara genellikle `COM` ile başvurursunuz. +XPipe ayrıca `COM` öneki olmadan sadece dizinin belirtilmesini de destekler. +9'dan büyük portları adreslemek için, `\\.\COM` ile UNC yol formunu kullanmanız gerekir. + +Eğer bir WSL1 dağıtımı yüklüyse, seri portlara WSL dağıtımı içinden `/dev/ttyS` ile de başvurabilirsiniz. +Ancak bu artık WSL2 ile çalışmamaktadır. +Eğer bir WSL1 sisteminiz varsa, bunu seri bağlantı için ana bilgisayar olarak kullanabilir ve XPipe ile erişmek için tty gösterimini kullanabilirsiniz. + +## Linux + +Linux sistemlerinde seri portlara genellikle `/dev/ttyS` üzerinden erişebilirsiniz. +Eğer bağlı cihazın ID'sini biliyorsanız ancak seri portu takip etmek istemiyorsanız, `/dev/serial/by-id/` üzerinden de referans verebilirsiniz. +`ls /dev/serial/by-id/*` komutunu çalıştırarak mevcut tüm seri bağlantı noktalarını kimlikleriyle birlikte listeleyebilirsiniz. + +## macOS + +MacOS'ta seri bağlantı noktası adları hemen hemen her şey olabilir, ancak genellikle `/dev/tty.` biçimindedir; burada id dahili aygıt tanımlayıcısıdır. +`ls /dev/tty.*` çalıştırıldığında mevcut seri portlar bulunacaktır. diff --git a/lang/proc/texts/serialPort_zh.md b/lang/proc/texts/serialPort_zh.md new file mode 100644 index 000000000..908b955b6 --- /dev/null +++ b/lang/proc/texts/serialPort_zh.md @@ -0,0 +1,20 @@ +## 窗口 + +在 Windows 系统中,您通常通过 `COM` 引用串行端口。 +XPipe 也支持只指定索引而不使用 `COM` 前缀。 +要寻址大于 9 的端口,您必须使用 UNC 路径形式,即 `\.\COM`。 + +如果安装了 WSL1 发行版,也可以在 WSL 发行版中通过 `/dev/ttyS` 引用串行端口。 +不过,这种方法在 WSL2 中不再适用。 +如果您有 WSL1 系统,可以将其作为串行连接的主机,并使用 tty 符号通过 XPipe 访问。 + +## Linux + +在 Linux 系统中,通常可以通过 `/dev/ttyS` 访问串口。 +如果您知道所连接设备的 ID,但不想跟踪串行端口,也可以通过 `/dev/serial/by-id/` 引用它们。 +运行 `ls /dev/serial/by-id/*` 可以列出所有可用串行端口及其 ID。 + +## macOS + +在 macOS 上,串行端口名称几乎可以是任何名称,但通常采用 `/dev/tty.` 的形式,其中 id 是内部设备标识符。 +运行 `ls /dev/tty.*` 可以找到可用的串行端口。 diff --git a/lang/uacc/strings/translations_da.properties b/lang/uacc/strings/translations_da.properties index d42a235c0..9701941a5 100644 --- a/lang/uacc/strings/translations_da.properties +++ b/lang/uacc/strings/translations_da.properties @@ -1,18 +1,18 @@ communityDescription=Et power-værktøj til forbindelser, der er perfekt til dit personlige brug. -professionalDescription=Professionel forbindelsesstyring til hele din serverinfrastruktur. -#custom -buyProfessional=Prøv XPipe Professional +upgradeDescription=Professionel forbindelsesstyring til hele din serverinfrastruktur. +discoverPlans=Opdag planer extendProfessional=Opgrader til de nyeste professionelle funktioner communityItem1=Ubegrænsede forbindelser til ikke-kommercielle systemer og værktøjer communityItem2=Problemfri integration med dine installerede terminaler og editorer communityItem3=Fuldt funktionsdygtig ekstern filbrowser communityItem4=Kraftfuldt scripting-system til alle shells communityItem5=Git-integration til synkronisering og deling af forbindelsesoplysninger -professionalItem1=Alle funktioner i community-udgaven -professionalItem2=Ubegrænsede forbindelser til alle kommercielle systemer og værktøjer -professionalItem3=Understøttelse af virksomhedsgodkendelsesordninger til fjernforbindelser -professionalItem4=Inkluderer alle fremtidige funktioner i Professional Edition i 1 år -professionalItem5=Modtager funktions- og sikkerhedsopdateringer for evigt +upgradeItem1=Inkluderer alle funktioner i community-udgaven +upgradeItem2=Homelab-planen understøtter et ubegrænset antal hypervisorer og avanceret SSH-auth +upgradeItem3=Den professionelle plan understøtter desuden virksomhedsoperativsystemer og -værktøjer +upgradeItem4=Virksomhedsplanen kommer med fuld fleksibilitet til din individuelle brugssag +upgrade=Opgradering +upgradeTitle=Tilgængelige planer status=Status type=Skriv licenseAlertTitle=Kommerciel brug diff --git a/lang/uacc/strings/translations_de.properties b/lang/uacc/strings/translations_de.properties index 9f981451f..c476c25f4 100644 --- a/lang/uacc/strings/translations_de.properties +++ b/lang/uacc/strings/translations_de.properties @@ -1,19 +1,18 @@ communityDescription=Ein Power-Tool für Verbindungen, das perfekt für deine persönlichen Anwendungsfälle ist. -professionalDescription=Professionelles Verbindungsmanagement für deine gesamte Serverinfrastruktur. -buyProfessional=Probiere XPipe professional aus +upgradeDescription=Professionelles Verbindungsmanagement für deine gesamte Serverinfrastruktur. +discoverPlans=Pläne entdecken extendProfessional=Upgrade auf die neuesten professionellen Funktionen communityItem1=Unbegrenzte Verbindungen zu nicht-kommerziellen Systemen und Tools communityItem2=Nahtlose Integration mit deinen installierten Terminals und Editoren communityItem3=Voll funktionsfähiger Remote-Dateibrowser communityItem4=Leistungsstarkes Skripting-System für alle Shells communityItem5=Git-Integration für die Synchronisierung und den Austausch von Verbindungsinformationen -professionalItem1=Alle Funktionen der Community Edition -professionalItem2=Unbegrenzte Verbindungen zu allen kommerziellen Systemen und Tools -#custom -professionalItem3=Unterstützung für Enterprise Authentifizierungssysteme für Verbindungen -professionalItem4=Beinhaltet alle zukünftigen Funktionen der Professional Edition für 1 Jahr -#custom -professionalItem5=Erhält kontinuierlich Funktions- und Sicherheitsupdates +upgradeItem1=Enthält alle Funktionen der Community Edition +upgradeItem2=Der Homelab-Plan unterstützt eine unbegrenzte Anzahl von Hypervisoren und erweiterte SSH-Authentifizierung +upgradeItem3=Der professionelle Plan unterstützt zusätzlich die Betriebssysteme und Tools von Unternehmen +upgradeItem4=Der Enterprise Plan bietet dir volle Flexibilität für deinen individuellen Anwendungsfall +upgrade=Upgrade +upgradeTitle=Verfügbare Pläne status=Status type=Typ licenseAlertTitle=Kommerzielle Nutzung diff --git a/lang/uacc/strings/translations_en.properties b/lang/uacc/strings/translations_en.properties index c6cdd5243..bea217085 100644 --- a/lang/uacc/strings/translations_en.properties +++ b/lang/uacc/strings/translations_en.properties @@ -1,17 +1,18 @@ communityDescription=A connection power-tool perfect for your personal use cases. -professionalDescription=Professional connection management for your entire server infrastructure. -buyProfessional=Try XPipe professional +upgradeDescription=Professional connection management for your entire server infrastructure. +discoverPlans=Discover plans extendProfessional=Upgrade to latest professional features communityItem1=Unlimited connections to non-commercial systems and tools communityItem2=Seamless integration with your installed terminals and editors communityItem3=Fully featured remote file browser communityItem4=Powerful scripting system for all shells communityItem5=Git integration for synchronization and sharing connection information -professionalItem1=All community edition features -professionalItem2=Unlimited connections to all commercial systems and tools -professionalItem3=Support for enterprise authentication schemes for remote connections -professionalItem4=Includes all future professional edition features for 1 year -professionalItem5=Receives feature and security updates forever +upgradeItem1=Include all community edition features +upgradeItem2=The homelab plan supports unlimited hypervisors and advanced SSH auth +upgradeItem3=The professional plan additionally supports enterprise operating systems and tools +upgradeItem4=The enterprise plan comes with full flexibility for your individual use case +upgrade=Upgrade +upgradeTitle=Available plans status=Status type=Type licenseAlertTitle=Commercial usage diff --git a/lang/uacc/strings/translations_es.properties b/lang/uacc/strings/translations_es.properties index 2e39a95be..632eed1e5 100644 --- a/lang/uacc/strings/translations_es.properties +++ b/lang/uacc/strings/translations_es.properties @@ -1,17 +1,18 @@ communityDescription=Una herramienta de conexión perfecta para tus casos de uso personal. -professionalDescription=Gestión profesional de conexiones para toda tu infraestructura de servidores. -buyProfessional=Prueba XPipe profesional +upgradeDescription=Gestión profesional de conexiones para toda tu infraestructura de servidores. +discoverPlans=Descubrir planes extendProfessional=Actualiza a las últimas funciones profesionales communityItem1=Conexiones ilimitadas a sistemas y herramientas no comerciales communityItem2=Integración perfecta con tus terminales y editores instalados communityItem3=Navegador de archivos remoto con todas las funciones communityItem4=Potente sistema de scripts para todos los shells communityItem5=Integración de Git para sincronizar y compartir información de conexión -professionalItem1=Todas las funciones de la edición comunitaria -professionalItem2=Conexiones ilimitadas a todos los sistemas y herramientas comerciales -professionalItem3=Soporte de esquemas de autenticación empresarial para conexiones remotas -professionalItem4=Incluye todas las futuras funciones de la edición profesional durante 1 año -professionalItem5=Recibe actualizaciones de funciones y seguridad para siempre +upgradeItem1=Incluye todas las funciones de la edición comunitaria +upgradeItem2=El plan homelab admite hipervisores ilimitados y autenticación SSH avanzada +upgradeItem3=El plan profesional admite además sistemas operativos y herramientas empresariales +upgradeItem4=El plan de empresa ofrece total flexibilidad para tu caso de uso individual +upgrade=Actualiza +upgradeTitle=Planes disponibles status=Estado type=Escribe licenseAlertTitle=Uso comercial diff --git a/lang/uacc/strings/translations_fr.properties b/lang/uacc/strings/translations_fr.properties index 3a1a6a03f..dd0b7ab9e 100644 --- a/lang/uacc/strings/translations_fr.properties +++ b/lang/uacc/strings/translations_fr.properties @@ -1,17 +1,18 @@ communityDescription=Un outil puissant de connexion parfait pour tes cas d'utilisation personnels. -professionalDescription=Gestion professionnelle des connexions pour l'ensemble de ton infrastructure de serveurs. -buyProfessional=Essaie XPipe professional +upgradeDescription=Gestion professionnelle des connexions pour l'ensemble de ton infrastructure de serveurs. +discoverPlans=Découvrir les plans extendProfessional=Mise à jour vers les dernières fonctionnalités professionnelles communityItem1=Connexions illimitées à des systèmes et outils non commerciaux communityItem2=Intégration transparente avec les terminaux et les éditeurs que tu as installés communityItem3=Navigateur de fichiers à distance complet communityItem4=Système de script puissant pour tous les shells communityItem5=Intégration Git pour la synchronisation et le partage des informations de connexion -professionalItem1=Toutes les fonctionnalités de l'édition communautaire -professionalItem2=Connexions illimitées à tous les systèmes et outils commerciaux -professionalItem3=Prise en charge des schémas d'authentification d'entreprise pour les connexions à distance -professionalItem4=Comprend toutes les futures fonctionnalités de l'édition professionnelle pendant 1 an -professionalItem5=Reçoit les mises à jour des fonctionnalités et de la sécurité pour toujours +upgradeItem1=Inclut toutes les fonctionnalités de l'édition communautaire +upgradeItem2=Le plan homelab prend en charge un nombre illimité d'hyperviseurs et l'authentification SSH avancée +upgradeItem3=Le plan professionnel prend en outre en charge les systèmes d'exploitation et les outils de l'entreprise +upgradeItem4=Le plan d'entreprise est assorti d'une flexibilité totale pour ton cas d'utilisation individuel +upgrade=Mise à niveau +upgradeTitle=Plans disponibles status=Statut type=Type de texte licenseAlertTitle=Usage commercial diff --git a/lang/uacc/strings/translations_it.properties b/lang/uacc/strings/translations_it.properties index 2183db882..d29f18488 100644 --- a/lang/uacc/strings/translations_it.properties +++ b/lang/uacc/strings/translations_it.properties @@ -1,17 +1,18 @@ communityDescription=Uno strumento di connessione perfetto per i tuoi casi d'uso personali. -professionalDescription=Gestione professionale delle connessioni per tutta la tua infrastruttura server. -buyProfessional=Prova XPipe professional +upgradeDescription=Gestione professionale delle connessioni per tutta la tua infrastruttura server. +discoverPlans=Scoprire i piani extendProfessional=Aggiornamento alle ultime funzionalità professionali communityItem1=Connessioni illimitate a sistemi e strumenti non commerciali communityItem2=Integrazione perfetta con i terminali e gli editor installati communityItem3=Browser di file remoto completo communityItem4=Potente sistema di scripting per tutte le shell communityItem5=Integrazione Git per la sincronizzazione e la condivisione delle informazioni di connessione -professionalItem1=Tutte le caratteristiche della community edition -professionalItem2=Connessioni illimitate a tutti i sistemi e gli strumenti commerciali -professionalItem3=Supporto per gli schemi di autenticazione aziendale per le connessioni remote -professionalItem4=Include tutte le funzioni future dell'edizione professionale per 1 anno -professionalItem5=Riceve aggiornamenti di funzionalità e sicurezza per sempre +upgradeItem1=Include tutte le funzionalità della community edition +upgradeItem2=Il piano Homelab supporta un numero illimitato di hypervisor e l'autenticazione SSH avanzata +upgradeItem3=Il piano professionale supporta anche i sistemi operativi e gli strumenti aziendali +upgradeItem4=Il piano enterprise è dotato di una flessibilità totale per i tuoi casi d'uso individuali +upgrade=Aggiornamento +upgradeTitle=Piani disponibili status=Stato type=Tipo licenseAlertTitle=Uso commerciale diff --git a/lang/uacc/strings/translations_ja.properties b/lang/uacc/strings/translations_ja.properties index 24a3a1aeb..f0a657a6c 100644 --- a/lang/uacc/strings/translations_ja.properties +++ b/lang/uacc/strings/translations_ja.properties @@ -1,17 +1,18 @@ communityDescription=個人的なユースケースに最適な接続パワーツール。 -professionalDescription=サーバーインフラ全体のプロフェッショナルな接続管理 -buyProfessional=XPipeプロフェッショナルを試す +upgradeDescription=サーバーインフラ全体のプロフェッショナルな接続管理 +discoverPlans=プランを発見する extendProfessional=最新のプロフェッショナル機能にアップグレードする communityItem1=非商用システムやツールに無制限に接続できる communityItem2=インストールされている端末やエディターとのシームレスな統合 communityItem3=フル機能のリモートファイルブラウザ communityItem4=すべてのシェルのための強力なスクリプトシステム communityItem5=接続情報の同期と共有のためのGitの統合 -professionalItem1=コミュニティ版の全機能 -professionalItem2=すべての商用システムとツールに無制限に接続できる -professionalItem3=リモート接続における企業認証スキームのサポート -professionalItem4=将来のプロフェッショナル版の全機能を1年間含む -professionalItem5=機能とセキュリティのアップデートを永久に受け取る +upgradeItem1=コミュニティ版のすべての機能を含む +upgradeItem2=ホームラボプランは無制限のハイパーバイザーと高度なSSH認証をサポートする +upgradeItem3=プロフェッショナル・プランでは、エンタープライズ・オペレーティング・システムとツールもサポートする。 +upgradeItem4=エンタープライズ・プランには、個々のユースケースに対応する柔軟性が備わっている。 +upgrade=アップグレード +upgradeTitle=利用可能なプラン status=ステータス type=タイプ licenseAlertTitle=商用利用 diff --git a/lang/uacc/strings/translations_nl.properties b/lang/uacc/strings/translations_nl.properties index 26512fa8d..56f4b04fc 100644 --- a/lang/uacc/strings/translations_nl.properties +++ b/lang/uacc/strings/translations_nl.properties @@ -1,17 +1,18 @@ communityDescription=Een power-tool voor verbindingen, perfect voor persoonlijk gebruik. -professionalDescription=Professioneel verbindingsbeheer voor je hele serverinfrastructuur. -buyProfessional=Probeer XPipe professional +upgradeDescription=Professioneel verbindingsbeheer voor je hele serverinfrastructuur. +discoverPlans=Plannen ontdekken extendProfessional=Upgrade naar de nieuwste professionele functies communityItem1=Onbeperkte verbindingen met niet-commerciële systemen en hulpmiddelen communityItem2=Naadloze integratie met je geïnstalleerde terminals en editors communityItem3=Volledig uitgeruste bestandsbrowser op afstand communityItem4=Krachtig scriptsysteem voor alle shells communityItem5=Git-integratie voor synchronisatie en het delen van verbindingsinformatie -professionalItem1=Alle community-editie functies -professionalItem2=Onbeperkte verbindingen met alle commerciële systemen en tools -professionalItem3=Ondersteuning voor bedrijfsverificatieschema's voor verbindingen op afstand -professionalItem4=Inclusief alle toekomstige professional edition functies voor 1 jaar -professionalItem5=Ontvangt altijd functie- en beveiligingsupdates +upgradeItem1=Omvat alle functies van de community-editie +upgradeItem2=Het homelab plan ondersteunt onbeperkte hypervisors en geavanceerde SSH auth +upgradeItem3=Het professionele plan ondersteunt bovendien bedrijfsbesturingssystemen en -tools +upgradeItem4=Het enterprise plan biedt volledige flexibiliteit voor jouw individuele gebruikssituatie +upgrade=Upgrade +upgradeTitle=Beschikbare plannen status=Status type=Type licenseAlertTitle=Commercieel gebruik diff --git a/lang/uacc/strings/translations_pt.properties b/lang/uacc/strings/translations_pt.properties index 2c0460e30..10b0e785f 100644 --- a/lang/uacc/strings/translations_pt.properties +++ b/lang/uacc/strings/translations_pt.properties @@ -1,17 +1,18 @@ communityDescription=Uma ferramenta de ligação perfeita para os teus casos de utilização pessoal. -professionalDescription=Gestão profissional das ligações para toda a tua infraestrutura de servidores. -buyProfessional=Experimenta o XPipe professional +upgradeDescription=Gestão profissional das ligações para toda a tua infraestrutura de servidores. +discoverPlans=Descobre planos extendProfessional=Actualiza para as funcionalidades profissionais mais recentes communityItem1=Ligações ilimitadas a sistemas e ferramentas não comerciais communityItem2=Integração perfeita com os teus terminais e editores instalados communityItem3=Navegador de ficheiros remoto com todas as funcionalidades communityItem4=Poderoso sistema de scripting para todos os shells communityItem5=Integração Git para sincronização e partilha de informações de ligação -professionalItem1=Todas as funcionalidades da edição comunitária -professionalItem2=Ligações ilimitadas a todos os sistemas e ferramentas comerciais -professionalItem3=Suporte para esquemas de autenticação empresarial para ligações remotas -professionalItem4=Inclui todas as funcionalidades da futura edição profissional durante 1 ano -professionalItem5=Recebe actualizações de funcionalidades e de segurança para sempre +upgradeItem1=Inclui todas as funcionalidades da edição comunitária +upgradeItem2=O plano homelab suporta hipervisores ilimitados e autenticação SSH avançada +upgradeItem3=O plano profissional suporta adicionalmente sistemas operativos e ferramentas empresariais +upgradeItem4=O plano empresarial inclui total flexibilidade para o teu caso de utilização individual +upgrade=Actualiza-te +upgradeTitle=Planos disponíveis status=Estado type=Digita licenseAlertTitle=Utilização comercial diff --git a/lang/uacc/strings/translations_ru.properties b/lang/uacc/strings/translations_ru.properties index 3a7574c9a..d7f4bf5dc 100644 --- a/lang/uacc/strings/translations_ru.properties +++ b/lang/uacc/strings/translations_ru.properties @@ -1,17 +1,18 @@ communityDescription=Инструмент для соединения, идеально подходящий для твоих личных целей. -professionalDescription=Профессиональное управление соединениями для всей твоей серверной инфраструктуры. -buyProfessional=Попробуй XPipe professional +upgradeDescription=Профессиональное управление соединениями для всей твоей серверной инфраструктуры. +discoverPlans=Узнай планы extendProfessional=Обновись до последних профессиональных функций communityItem1=Неограниченное количество подключений к некоммерческим системам и инструментам communityItem2=Бесшовная интеграция с установленными у тебя терминалами и редакторами communityItem3=Полнофункциональный браузер удаленных файлов communityItem4=Мощная система скриптов для всех оболочек communityItem5=Интеграция Git для синхронизации и обмена информацией о соединениях -professionalItem1=Все возможности community edition -professionalItem2=Неограниченное подключение ко всем коммерческим системам и инструментам -professionalItem3=Поддержка корпоративных схем аутентификации для удаленных подключений -professionalItem4=Включает в себя все будущие возможности профессионального издания на 1 год -professionalItem5=Получает обновления функций и безопасности навсегда +upgradeItem1=Включи все возможности community edition +upgradeItem2=План homelab поддерживает неограниченное количество гипервизоров и расширенный SSH-аутинг +upgradeItem3=Профессиональный план дополнительно поддерживает корпоративные операционные системы и инструменты +upgradeItem4=Корпоративный тарифный план обеспечивает полную гибкость для твоего индивидуального использования +upgrade=Обновление +upgradeTitle=Доступные планы status=Статус type=Тип licenseAlertTitle=Коммерческое использование diff --git a/lang/uacc/strings/translations_tr.properties b/lang/uacc/strings/translations_tr.properties index c7b0b1134..fd6e75cf4 100644 --- a/lang/uacc/strings/translations_tr.properties +++ b/lang/uacc/strings/translations_tr.properties @@ -1,17 +1,18 @@ communityDescription=Kişisel kullanım durumlarınız için mükemmel bir bağlantı güç aracı. -professionalDescription=Tüm sunucu altyapınız için profesyonel bağlantı yönetimi. -buyProfessional=XPipe professional'ı deneyin +upgradeDescription=Tüm sunucu altyapınız için profesyonel bağlantı yönetimi. +discoverPlans=Planları keşfedin extendProfessional=En son profesyonel özelliklere yükseltme communityItem1=Ticari olmayan sistemlere ve araçlara sınırsız bağlantı communityItem2=Kurulu terminalleriniz ve editörlerinizle sorunsuz entegrasyon communityItem3=Tam özellikli uzak dosya tarayıcısı communityItem4=Tüm kabuklar için güçlü komut dosyası sistemi communityItem5=Senkronizasyon ve bağlantı bilgilerinin paylaşımı için Git entegrasyonu -professionalItem1=Tüm topluluk sürümü özellikleri -professionalItem2=Tüm ticari sistemlere ve araçlara sınırsız bağlantı -professionalItem3=Uzak bağlantılar için kurumsal kimlik doğrulama şemaları desteği -professionalItem4=Gelecekteki tüm profesyonel sürüm özelliklerini 1 yıl boyunca içerir -professionalItem5=Özellik ve güvenlik güncellemelerini sonsuza kadar alır +upgradeItem1=Tüm topluluk sürümü özelliklerini dahil edin +upgradeItem2=Homelab planı sınırsız hipervizörü ve gelişmiş SSH kimlik doğrulamasını destekler +upgradeItem3=Profesyonel plan ayrıca kurumsal işletim sistemlerini ve araçlarını da destekler +upgradeItem4=Kurumsal plan, bireysel kullanım durumunuz için tam esneklikle birlikte gelir +upgrade=Yükseltme +upgradeTitle=Mevcut planlar status=Durum type=Tip licenseAlertTitle=Ticari kullanım diff --git a/lang/uacc/strings/translations_zh.properties b/lang/uacc/strings/translations_zh.properties index 9234d1e04..500b58885 100644 --- a/lang/uacc/strings/translations_zh.properties +++ b/lang/uacc/strings/translations_zh.properties @@ -1,17 +1,18 @@ communityDescription=最适合您个人使用的连接动力工具。 -professionalDescription=为您的整个服务器基础设施提供专业的连接管理。 -buyProfessional=试用 XPipe 专业版 +upgradeDescription=为您的整个服务器基础设施提供专业的连接管理。 +discoverPlans=发现计划 extendProfessional=升级到最新的专业功能 communityItem1=无限连接非商业系统和工具 communityItem2=与已安装的终端和编辑器无缝集成 communityItem3=功能齐全的远程文件浏览器 communityItem4=适用于所有 shell 的强大脚本系统 communityItem5=Git 集成,用于同步和共享连接信息 -professionalItem1=所有社区版功能 -professionalItem2=无限连接所有商业系统和工具 -professionalItem3=支持用于远程连接的企业身份验证方案 -professionalItem4=包括未来 1 年专业版的所有功能 -professionalItem5=永久接收功能和安全更新 +upgradeItem1=包括所有社区版功能 +upgradeItem2=家庭实验室计划支持无限的管理程序和高级 SSH 验证 +upgradeItem3=专业计划还支持企业操作系统和工具 +upgradeItem4=企业计划具有充分的灵活性,可满足您的个性化需求 +upgrade=升级 +upgradeTitle=可用计划 status=状态 type=类型 licenseAlertTitle=商业用途 diff --git a/openapi.yaml b/openapi.yaml index d8547f806..1db378cd2 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -55,11 +55,14 @@ paths: $ref: '#/components/schemas/HandshakeRequest' examples: standard: - summary: Standard handshake + summary: API key handshake value: { "auth": { "type": "ApiKey", "key": "" }, "client": { "type": "Api", "name": "My client name" } } local: summary: Local application handshake - value: { "auth": { "type": "Local", "authFileContent": "" }, "client": { "type": "Api", "name": "My client name" } } + value: { "auth": { "type": "Local", "authFileContent": "/xpipe_auth>" }, "client": { "type": "Api", "name": "My client name" } } + local-ptb: + summary: Local PTB application handshake + value: { "auth": { "type": "Local", "authFileContent": "/xpipe_ptb_auth>" }, "client": { "type": "Api", "name": "My client name" } } responses: '200': description: The handshake was successful. The returned token can be used for authentication in this session. The token is valid as long as XPipe is running. @@ -624,6 +627,9 @@ components: osName: type: string description: The display name of the operating system + ttyState: + type: string + description: Whether a tty/pty has been allocated for the connection. If allocated, input and output will be unreliable. It is not recommended to use a shell connection then. temp: type: string description: The location of the temporary directory @@ -966,14 +972,24 @@ components: - key - type Local: - description: Authentication method for local applications. Uses file system access as proof of authentication. + description: | + Authentication method for local applications. Uses file system access as proof of authentication. + + You can find the authentication file at: + - %TEMP%\xpipe_auth on Windows + - $TMP/xpipe_auth on Linux + - $TMPDIR/xpipe_auth on macOS + + For the PTB releases the file name is changed to xpipe_ptb_auth to prevent collisions. + + As the temporary directory on Linux is global, the daemon might run as another user and your current user might not have permissions to access the auth file. type: object properties: type: type: string authFileContent: type: string - description: The contents of the local file $TEMP/xpipe_auth. This file is automatically generated when XPipe starts. + description: The contents of the local file /xpipe_auth. This file is automatically generated when XPipe starts. required: - authFileContent - type diff --git a/version b/version index 924c68fa4..4923475f9 100644 --- a/version +++ b/version @@ -1 +1 @@ -10.1-6 +11.0-6