diff --git a/app/build.gradle b/app/build.gradle index d2a337a1..b7788035 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -45,20 +45,20 @@ dependencies { api 'com.vladsch.flexmark:flexmark-ext-footnotes:0.64.8' api 'com.vladsch.flexmark:flexmark-ext-definition:0.64.8' api 'com.vladsch.flexmark:flexmark-ext-anchorlink:0.64.8' + api 'com.vladsch.flexmark:flexmark-ext-yaml-front-matter:0.64.8' + api 'com.vladsch.flexmark:flexmark-ext-toc:0.64.8' api files("$rootDir/gradle/gradle_scripts/markdowngenerator-1.3.1.1.jar") api files("$rootDir/gradle/gradle_scripts/vernacular-1.16.jar") - api 'info.picocli:picocli:4.7.5' + api 'info.picocli:picocli:4.7.6' api ('org.kohsuke:github-api:1.321') { exclude group: 'org.apache.commons', module: 'commons-lang3' } api 'org.apache.commons:commons-lang3:3.14.0' - api 'io.sentry:sentry:7.8.0' + api 'io.sentry:sentry:7.10.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.module', name: 'jackson-module-parameter-names', version: "2.17.1" api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.17.1" - api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jdk8', version: "2.17.1" api group: 'org.kordamp.ikonli', name: 'ikonli-material2-pack', version: "12.2.0" api group: 'org.kordamp.ikonli', name: 'ikonli-materialdesign2-pack', version: "12.2.0" api group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0" @@ -68,10 +68,7 @@ dependencies { api group: 'org.slf4j', name: 'slf4j-jdk-platform-logging', version: '2.0.13' api 'io.xpipe:modulefs:0.1.5' api 'net.synedra:validatorfx:0.4.2' - api ('io.github.mkpaz:atlantafx-base:2.0.1') { - exclude group: 'org.openjfx', module: 'javafx-base' - exclude group: 'org.openjfx', module: 'javafx-controls' - } + api files("$rootDir/gradle/gradle_scripts/atlantafx-base-2.0.2.jar") } apply from: "$rootDir/gradle/gradle_scripts/local_junit_suite.gradle" @@ -96,9 +93,6 @@ 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", "21724" - // systemProperty "io.xpipe.beacon.printMessages", "true" - // systemProperty 'io.xpipe.app.debugPlatform', "true" // Apply passed xpipe properties for (final def e in System.getProperties().entrySet()) { @@ -153,6 +147,13 @@ processResources { into resourcesDir } } + + doLast { + copy { + from file("$rootDir/openapi.yaml") + into file("${sourceSets.main.output.resourcesDir}/io/xpipe/app/resources/misc"); + } + } } distTar { diff --git a/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java b/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java new file mode 100644 index 00000000..68fcc7fd --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java @@ -0,0 +1,174 @@ +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.prefs.AppPrefs; +import io.xpipe.app.util.MarkdownHelper; +import io.xpipe.beacon.BeaconConfig; +import io.xpipe.beacon.BeaconInterface; +import io.xpipe.core.util.XPipeInstallation; +import lombok.Getter; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.*; +import java.util.concurrent.Executors; + +public class AppBeaconServer { + + private static AppBeaconServer INSTANCE; + @Getter + private final int port; + @Getter + private final boolean propertyPort; + private boolean running; + private HttpServer server; + @Getter + private final Set sessions = new HashSet<>(); + @Getter + private final Set shellSessions = new HashSet<>(); + @Getter + private String localAuthSecret; + + private String notFoundHtml; + private final Map resources = new HashMap<>(); + + static { + int port; + boolean propertyPort; + if (System.getProperty(BeaconConfig.BEACON_PORT_PROP) != null) { + port = BeaconConfig.getUsedPort(); + propertyPort = true; + } else { + port = AppPrefs.get().httpServerPort().getValue(); + propertyPort = false; + } + INSTANCE = new AppBeaconServer(port, propertyPort); + } + + private AppBeaconServer(int port, boolean propertyPort) { + this.port = port; + this.propertyPort = propertyPort; + } + + public static void init() { + try { + INSTANCE.initAuthSecret(); + INSTANCE.start(); + TrackEvent.withInfo("Started http server") + .tag("port", INSTANCE.getPort()) + .build() + .handle(); + } catch (Exception ex) { + // Not terminal! + // We can still continue without the running server + ErrorEvent.fromThrowable("Unable to start local http server on port " + INSTANCE.getPort(), ex) + .build() + .handle(); + } + } + + public static void reset() { + if (INSTANCE != null) { + INSTANCE.stop(); + INSTANCE = null; + } + } + + public void addSession(BeaconSession session) { + this.sessions.add(session); + } + + public static AppBeaconServer get() { + return INSTANCE; + } + + private void stop() { + if (!running) { + return; + } + + running = false; + server.stop(1); + } + + private void initAuthSecret() throws IOException { + var file = XPipeInstallation.getLocalBeaconAuthFile(); + var id = UUID.randomUUID().toString(); + Files.writeString(file, id); + localAuthSecret = id; + } + + private void start() throws IOException { + server = HttpServer.create(new InetSocketAddress("localhost", port), 10); + BeaconInterface.getAll().forEach(beaconInterface -> { + server.createContext(beaconInterface.getPath(), new BeaconRequestHandler<>(beaconInterface)); + }); + server.setExecutor(Executors.newSingleThreadExecutor(r -> { + Thread t = Executors.defaultThreadFactory().newThread(r); + t.setName("http handler"); + t.setUncaughtExceptionHandler((t1, e) -> { + ErrorEvent.fromThrowable(e).handle(); + }); + return t; + })); + + var resourceMap = Map.of( + "openapi.yaml", "misc/openapi.yaml", + "markdown.css", "misc/github-markdown-dark.css", + "highlight.min.js", "misc/highlight.min.js", + "github-dark.min.css", "misc/github-dark.min.css" + ); + resourceMap.forEach((s, s2) -> { + server.createContext("/" + s, exchange -> { + handleResource(exchange, s2); + }); + }); + + server.createContext("/", exchange -> { + handleCatchAll(exchange); + }); + + server.start(); + running = true; + } + + private void handleResource(HttpExchange exchange, String resource) throws IOException { + if (!resources.containsKey(resource)) { + AppResources.with(AppResources.XPIPE_MODULE, resource, file -> { + resources.put(resource, Files.readString(file)); + }); + } + var body = resources.get(resource).getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200,body.length); + try (var out = exchange.getResponseBody()) { + out.write(body); + } + } + + private void handleCatchAll(HttpExchange exchange) throws IOException { + if (notFoundHtml == null) { + AppResources.with(AppResources.XPIPE_MODULE, "misc/api.md", file -> { + notFoundHtml = MarkdownHelper.toHtml(Files.readString(file), head -> { + return head + "\n" + + "" + "\n" + + "" + "\n" + + "" + "\n" + + ""; + }, s -> { + return "
" + s + "
"; + }); + }); + } + var body = notFoundHtml.getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200,body.length); + try (var out = exchange.getResponseBody()) { + out.write(body); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java b/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java new file mode 100644 index 00000000..ce713fe7 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java @@ -0,0 +1,124 @@ +package io.xpipe.app.beacon; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.issue.TrackEvent; +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.beacon.*; +import io.xpipe.core.util.JacksonMapper; +import lombok.SneakyThrows; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +public class BeaconRequestHandler implements HttpHandler { + + private final BeaconInterface beaconInterface; + + public BeaconRequestHandler(BeaconInterface beaconInterface) {this.beaconInterface = beaconInterface;} + + @Override + public void handle(HttpExchange exchange) throws IOException { + if (!AppPrefs.get().disableApiAuthentication().get() && beaconInterface.requiresAuthentication()) { + var auth = exchange.getRequestHeaders().getFirst("Authorization"); + if (auth == null) { + writeError(exchange, new BeaconClientErrorResponse("Missing Authorization header"), 401); + return; + } + + var token = auth.replace("Bearer ", ""); + var session = AppBeaconServer.get().getSessions().stream().filter(s -> s.getToken().equals(token)).findFirst().orElse(null); + if (session == null) { + writeError(exchange, new BeaconClientErrorResponse("Unknown token"), 403); + return; + } + } + + handleAuthenticatedRequest(exchange); + } + + private void handleAuthenticatedRequest(HttpExchange exchange) { + T object; + Object response; + try { + try (InputStream is = exchange.getRequestBody()) { + var tree = JacksonMapper.getDefault().readTree(is); + TrackEvent.trace("Parsed raw request:\n" + tree.toPrettyString()); + var emptyRequestClass = tree.isEmpty() && beaconInterface.getRequestClass().getDeclaredFields().length == 0; + object = emptyRequestClass ? createDefaultRequest(beaconInterface) : JacksonMapper.getDefault().treeToValue(tree, beaconInterface.getRequestClass()); + TrackEvent.trace("Parsed request object:\n" + object); + } + response = beaconInterface.handle(exchange, object); + } catch (BeaconClientException clientException) { + ErrorEvent.fromThrowable(clientException).omit().expected().handle(); + writeError(exchange, new BeaconClientErrorResponse(clientException.getMessage()), 400); + return; + } catch (BeaconServerException serverException) { + var cause = serverException.getCause() != null ? serverException.getCause() : serverException; + ErrorEvent.fromThrowable(cause).handle(); + writeError(exchange, new BeaconServerErrorResponse(cause), 500); + return; + } catch (IOException ex) { + // Handle serialization errors as normal exceptions and other IO exceptions as assuming that the connection is broken + if (!ex.getClass().getName().contains("jackson")) { + ErrorEvent.fromThrowable(ex).omit().expected().handle(); + } else { + ErrorEvent.fromThrowable(ex).omit().expected().handle(); + writeError(exchange, new BeaconClientErrorResponse(ex.getMessage()), 400); + } + return; + } catch (Throwable other) { + ErrorEvent.fromThrowable(other).handle(); + writeError(exchange, new BeaconServerErrorResponse(other), 500); + return; + } + + try { + var emptyResponseClass = beaconInterface.getResponseClass().getDeclaredFields().length == 0; + if (!emptyResponseClass && response != null) { + TrackEvent.trace("Sending response:\n" + object); + var tree = JacksonMapper.getDefault().valueToTree(response); + TrackEvent.trace("Sending raw response:\n" + tree.toPrettyString()); + var bytes = tree.toPrettyString().getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, bytes.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(bytes); + } + } else { + exchange.sendResponseHeaders(200, -1); + } + } catch (IOException ioException) { + ErrorEvent.fromThrowable(ioException).omit().expected().handle(); + } catch (Throwable other) { + ErrorEvent.fromThrowable(other).handle(); + writeError(exchange, new BeaconServerErrorResponse(other), 500); + return; + } + } + + private void writeError(HttpExchange exchange, Object errorMessage, int code) { + try { + var bytes = JacksonMapper.getDefault().writeValueAsString(errorMessage).getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(code, bytes.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(bytes); + } + } catch (IOException ex) { + ErrorEvent.fromThrowable(ex).omit().expected().handle(); + } + } + + @SneakyThrows + @SuppressWarnings("unchecked") + private REQ createDefaultRequest(BeaconInterface beaconInterface) { + var c = beaconInterface.getRequestClass().getDeclaredMethod("builder"); + c.setAccessible(true); + var b = c.invoke(null); + var m = b.getClass().getDeclaredMethod("build"); + m.setAccessible(true); + return (REQ) beaconInterface.getRequestClass().cast(m.invoke(b)); + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/BeaconSession.java b/app/src/main/java/io/xpipe/app/beacon/BeaconSession.java new file mode 100644 index 00000000..38cec0f2 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconSession.java @@ -0,0 +1,11 @@ +package io.xpipe.app.beacon; + +import io.xpipe.beacon.BeaconClientInformation; +import lombok.Value; + +@Value +public class BeaconSession { + + BeaconClientInformation clientInformation; + String token; +} diff --git a/app/src/main/java/io/xpipe/app/beacon/BeaconShellSession.java b/app/src/main/java/io/xpipe/app/beacon/BeaconShellSession.java new file mode 100644 index 00000000..a92415a7 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/BeaconShellSession.java @@ -0,0 +1,12 @@ +package io.xpipe.app.beacon; + +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.core.process.ShellControl; +import lombok.Value; + +@Value +public class BeaconShellSession { + + DataStoreEntry entry; + ShellControl control; +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java new file mode 100644 index 00000000..e3fa224b --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java @@ -0,0 +1,34 @@ +package io.xpipe.app.beacon.impl; + +import com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.util.AskpassAlert; +import io.xpipe.app.util.SecretManager; +import io.xpipe.beacon.BeaconClientException; +import io.xpipe.beacon.BeaconServerException; +import io.xpipe.beacon.api.AskpassExchange; + +import java.io.IOException; + +public class AskpassExchangeImpl extends AskpassExchange { + + @Override + public Object handle(HttpExchange exchange, Request msg) throws IOException, BeaconClientException, BeaconServerException { + if (msg.getRequest() == null) { + var r = AskpassAlert.queryRaw(msg.getPrompt(), null); + return Response.builder().value(r.getSecret()).build(); + } + + var found = msg.getSecretId() != null + ? SecretManager.getProgress(msg.getRequest(), msg.getSecretId()) + : SecretManager.getProgress(msg.getRequest()); + if (found.isEmpty()) { + return Response.builder().build(); + } + + var p = found.get(); + var secret = p.process(msg.getPrompt()); + return Response.builder() + .value(secret != null ? secret.inPlace() : null) + .build(); + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionQueryExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionQueryExchangeImpl.java new file mode 100644 index 00000000..2fca86f7 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ConnectionQueryExchangeImpl.java @@ -0,0 +1,148 @@ +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.BeaconServerException; +import io.xpipe.beacon.api.ConnectionQueryExchange; +import io.xpipe.core.store.StorePath; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public class ConnectionQueryExchangeImpl extends ConnectionQueryExchange { + + @Override + public Object handle(HttpExchange exchange, Request msg) throws IOException, BeaconClientException, BeaconServerException { + var catMatcher = Pattern.compile(toRegex("all connections/" + msg.getCategoryFilter())); + var conMatcher = Pattern.compile(toRegex(msg.getConnectionFilter())); + + List found = new ArrayList<>(); + for (DataStoreEntry storeEntry : DataStorage.get().getStoreEntries()) { + if (!storeEntry.getValidity().isUsable()) { + continue; + } + + var name = DataStorage.get().getStorePath(storeEntry).toString(); + if (!conMatcher.matcher(name).matches()) { + continue; + } + + var cat = DataStorage.get().getStoreCategoryIfPresent(storeEntry.getCategoryUuid()).orElse(null); + if (cat == null) { + continue; + } + + var c = DataStorage.get().getStorePath(cat).toString(); + if (!catMatcher.matcher(c).matches()) { + continue; + } + + found.add(storeEntry); + } + + var mapped = new ArrayList(); + for (DataStoreEntry e : found) { + var names = DataStorage.get().getStorePath(DataStorage.get().getStoreCategoryIfPresent(e.getCategoryUuid()).orElseThrow()).getNames(); + var cat = new StorePath(names.subList(1, names.size())); + var obj = ConnectionQueryExchange.QueryResponse.builder() + .uuid(e.getUuid()).category(cat).connection(DataStorage.get() + .getStorePath(e)).type(e.getProvider().getId()).build(); + mapped.add(obj); + } + return Response.builder().found(mapped).build(); + } + + private String toRegex(String pattern) { + // https://stackoverflow.com/a/17369948/6477761 + StringBuilder sb = new StringBuilder(pattern.length()); + int inGroup = 0; + int inClass = 0; + int firstIndexInClass = -1; + char[] arr = pattern.toCharArray(); + for (int i = 0; i < arr.length; i++) { + char ch = arr[i]; + switch (ch) { + case '\\': + if (++i >= arr.length) { + sb.append('\\'); + } else { + char next = arr[i]; + switch (next) { + case ',': + // escape not needed + break; + case 'Q': + case 'E': + // extra escape needed + sb.append('\\'); + default: + sb.append('\\'); + } + sb.append(next); + } + break; + case '*': + if (inClass == 0) + sb.append(".*"); + else + sb.append('*'); + break; + case '?': + if (inClass == 0) + sb.append('.'); + else + sb.append('?'); + break; + case '[': + inClass++; + firstIndexInClass = i+1; + sb.append('['); + break; + case ']': + inClass--; + sb.append(']'); + break; + case '.': + case '(': + case ')': + case '+': + case '|': + case '^': + case '$': + case '@': + case '%': + if (inClass == 0 || (firstIndexInClass == i && ch == '^')) + sb.append('\\'); + sb.append(ch); + break; + case '!': + if (firstIndexInClass == i) + sb.append('^'); + else + sb.append('!'); + break; + case '{': + inGroup++; + sb.append('('); + break; + case '}': + inGroup--; + sb.append(')'); + break; + case ',': + if (inGroup > 0) + sb.append('|'); + else + sb.append(','); + break; + default: + sb.append(ch); + } + } + return sb.toString(); + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonFocusExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonFocusExchangeImpl.java new file mode 100644 index 00000000..1a25af89 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonFocusExchangeImpl.java @@ -0,0 +1,20 @@ +package io.xpipe.app.beacon.impl; + +import com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.core.mode.OperationMode; +import io.xpipe.beacon.BeaconClientException; +import io.xpipe.beacon.BeaconServerException; +import io.xpipe.beacon.api.DaemonFocusExchange; + +import java.io.IOException; + +public class DaemonFocusExchangeImpl extends DaemonFocusExchange { + + +@Override +public Object handle(HttpExchange exchange, Request msg) throws IOException, BeaconClientException, BeaconServerException { + + OperationMode.switchUp(OperationMode.map(msg.getMode())); + return Response.builder().build(); +} +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonModeExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonModeExchangeImpl.java new file mode 100644 index 00000000..ab7de995 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonModeExchangeImpl.java @@ -0,0 +1,36 @@ +package io.xpipe.app.beacon.impl; + +import com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.core.mode.OperationMode; +import io.xpipe.app.util.ThreadHelper; +import io.xpipe.beacon.BeaconClientException; +import io.xpipe.beacon.BeaconServerException; +import io.xpipe.beacon.api.DaemonModeExchange; + +import java.io.IOException; + +public class DaemonModeExchangeImpl extends DaemonModeExchange { + @Override + public Object handle(HttpExchange exchange, Request msg) throws IOException, BeaconClientException, BeaconServerException { + // Wait for startup + while (OperationMode.get() == null) { + ThreadHelper.sleep(100); + } + + var mode = OperationMode.map(msg.getMode()); + if (!mode.isSupported()) { + throw new BeaconClientException("Unsupported mode: " + msg.getMode().getDisplayName() + ". Supported: " + + String.join( + ", ", + OperationMode.getAll().stream() + .filter(OperationMode::isSupported) + .map(OperationMode::getId) + .toList())); + } + + OperationMode.switchToSyncIfPossible(mode); + return DaemonModeExchange.Response.builder() + .usedMode(OperationMode.map(OperationMode.get())) + .build(); + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonOpenExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonOpenExchangeImpl.java new file mode 100644 index 00000000..8a89878e --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonOpenExchangeImpl.java @@ -0,0 +1,25 @@ +package io.xpipe.app.beacon.impl; + +import com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.core.mode.OperationMode; +import io.xpipe.app.launcher.LauncherInput; +import io.xpipe.app.util.PlatformState; +import io.xpipe.beacon.BeaconClientException; +import io.xpipe.beacon.BeaconServerException; +import io.xpipe.beacon.api.DaemonOpenExchange; + +import java.io.IOException; + +public class DaemonOpenExchangeImpl extends DaemonOpenExchange { + @Override + public Object handle(HttpExchange exchange, Request msg) throws IOException, BeaconClientException, BeaconServerException { + if (msg.getArguments().isEmpty()) { + if (!OperationMode.switchToSyncIfPossible(OperationMode.GUI)) { + throw new BeaconServerException(PlatformState.getLastError()); + } + } + + LauncherInput.handle(msg.getArguments()); + return Response.builder().build(); + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonStatusExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonStatusExchangeImpl.java new file mode 100644 index 00000000..77eadf15 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonStatusExchangeImpl.java @@ -0,0 +1,25 @@ +package io.xpipe.app.beacon.impl; + + +import com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.core.mode.OperationMode; +import io.xpipe.beacon.BeaconClientException; +import io.xpipe.beacon.BeaconServerException; +import io.xpipe.beacon.api.DaemonStatusExchange; + +import java.io.IOException; + +public class DaemonStatusExchangeImpl extends DaemonStatusExchange { + + @Override + public Object handle(HttpExchange exchange, Request body) throws IOException, BeaconClientException, BeaconServerException { + String mode; + if (OperationMode.get() == null) { + mode = "none"; + } else { + mode = OperationMode.get().getId(); + } + + return Response.builder().mode(mode).build(); + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonStopExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonStopExchangeImpl.java new file mode 100644 index 00000000..0189350c --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonStopExchangeImpl.java @@ -0,0 +1,22 @@ +package io.xpipe.app.beacon.impl; + +import com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.core.mode.OperationMode; +import io.xpipe.app.util.ThreadHelper; +import io.xpipe.beacon.BeaconClientException; +import io.xpipe.beacon.BeaconServerException; +import io.xpipe.beacon.api.DaemonStopExchange; + +import java.io.IOException; + +public class DaemonStopExchangeImpl extends DaemonStopExchange { + + @Override + public Object handle(HttpExchange exchange, Request msg) throws IOException, BeaconClientException, BeaconServerException { + ThreadHelper.runAsync(() -> { + ThreadHelper.sleep(1000); + OperationMode.close(); + }); + return Response.builder().success(true).build(); + } +} diff --git a/app/src/main/java/io/xpipe/app/exchange/VersionExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonVersionExchangeImpl.java similarity index 53% rename from app/src/main/java/io/xpipe/app/exchange/VersionExchangeImpl.java rename to app/src/main/java/io/xpipe/app/beacon/impl/DaemonVersionExchangeImpl.java index 9115b06f..f6ba986c 100644 --- a/app/src/main/java/io/xpipe/app/exchange/VersionExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonVersionExchangeImpl.java @@ -1,14 +1,17 @@ -package io.xpipe.app.exchange; +package io.xpipe.app.beacon.impl; +import com.sun.net.httpserver.HttpExchange; import io.xpipe.app.core.AppProperties; -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.exchange.cli.VersionExchange; +import io.xpipe.beacon.BeaconClientException; +import io.xpipe.beacon.BeaconServerException; +import io.xpipe.beacon.api.DaemonVersionExchange; -public class VersionExchangeImpl extends VersionExchange - implements MessageExchangeImpl { +import java.io.IOException; + +public class DaemonVersionExchangeImpl extends DaemonVersionExchange { @Override - public Response handleRequest(BeaconHandler handler, Request msg) { + public Object handle(HttpExchange exchange, Request msg) throws IOException, BeaconClientException, BeaconServerException { var jvmVersion = System.getProperty("java.vm.vendor") + " " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.version") + ")"; diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/HandshakeExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/HandshakeExchangeImpl.java new file mode 100644 index 00000000..2e10a773 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/impl/HandshakeExchangeImpl.java @@ -0,0 +1,42 @@ +package io.xpipe.app.beacon.impl; + + +import com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.beacon.AppBeaconServer; +import io.xpipe.app.beacon.BeaconSession; +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.beacon.BeaconAuthMethod; +import io.xpipe.beacon.BeaconClientException; +import io.xpipe.beacon.BeaconServerException; +import io.xpipe.beacon.api.HandshakeExchange; + +import java.io.IOException; +import java.util.UUID; + +public class HandshakeExchangeImpl extends HandshakeExchange { + + @Override + public Object handle(HttpExchange exchange, Request body) throws IOException, BeaconClientException, BeaconServerException { + if (!checkAuth(body.getAuth())) { + throw new BeaconClientException("Authentication failed"); + } + + var session = new BeaconSession(body.getClient(), UUID.randomUUID().toString()); + AppBeaconServer.get().addSession(session); + return Response.builder().sessionToken(session.getToken()).build(); + } + + private boolean checkAuth(BeaconAuthMethod authMethod) { + if (authMethod instanceof BeaconAuthMethod.Local local) { + var c = local.getAuthFileContent().trim(); + return AppBeaconServer.get().getLocalAuthSecret().equals(c); + } + + if (authMethod instanceof BeaconAuthMethod.ApiKey key) { + var c = key.getKey().trim(); + return AppPrefs.get().apiKey().get().equals(c); + } + + return false; + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ShellExecExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ShellExecExchangeImpl.java new file mode 100644 index 00000000..ad72fed6 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ShellExecExchangeImpl.java @@ -0,0 +1,35 @@ +package io.xpipe.app.beacon.impl; + +import com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.beacon.AppBeaconServer; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.beacon.BeaconClientException; +import io.xpipe.beacon.BeaconServerException; +import io.xpipe.beacon.api.ShellExecExchange; +import lombok.SneakyThrows; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicReference; + +public class ShellExecExchangeImpl extends ShellExecExchange { + + @Override + @SneakyThrows + public Object handle(HttpExchange exchange, Request msg) throws IOException, BeaconClientException, BeaconServerException { + var e = DataStorage.get().getStoreEntryIfPresent(msg.getConnection()).orElseThrow(() -> new IllegalArgumentException("Unknown connection")); + var existing = AppBeaconServer.get().getShellSessions().stream().filter(beaconShellSession -> beaconShellSession.getEntry().equals(e)).findFirst(); + if (existing.isEmpty()) { + throw new BeaconClientException("No shell session active for connection"); + } + + AtomicReference out = new AtomicReference<>(); + AtomicReference err = new AtomicReference<>(); + long exitCode; + try (var command = existing.get().getControl().command(msg.getCommand()).start()) { + command.accumulateStdout(s -> out.set(s)); + command.accumulateStderr(s -> err.set(s)); + exitCode = command.getExitCode(); + } + return Response.builder().stdout(out.get()).stderr(err.get()).exitCode(exitCode).build(); + } +} 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 new file mode 100644 index 00000000..085b8e71 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ShellStartExchangeImpl.java @@ -0,0 +1,34 @@ +package io.xpipe.app.beacon.impl; + +import com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.beacon.AppBeaconServer; +import io.xpipe.app.beacon.BeaconShellSession; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.beacon.BeaconClientException; +import io.xpipe.beacon.BeaconServerException; +import io.xpipe.beacon.api.ShellStartExchange; +import io.xpipe.core.store.ShellStore; +import lombok.SneakyThrows; + +import java.io.IOException; + +public class ShellStartExchangeImpl extends ShellStartExchange { + + @Override + @SneakyThrows + public Object handle(HttpExchange exchange, Request msg) throws IOException, BeaconClientException, BeaconServerException { + var e = DataStorage.get().getStoreEntryIfPresent(msg.getConnection()).orElseThrow(() -> new IllegalArgumentException("Unknown connection")); + if (!(e.getStore() instanceof ShellStore s)) { + throw new BeaconClientException("Not a shell connection"); + } + + var existing = AppBeaconServer.get().getShellSessions().stream().filter(beaconShellSession -> beaconShellSession.getEntry().equals(e)).findFirst(); + if (existing.isPresent()) { + return Response.builder().build(); + } + + var control = s.control().start(); + AppBeaconServer.get().getShellSessions().add(new BeaconShellSession(e, control)); + return Response.builder().build(); + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/ShellStopExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/ShellStopExchangeImpl.java new file mode 100644 index 00000000..07f07eb8 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/impl/ShellStopExchangeImpl.java @@ -0,0 +1,26 @@ +package io.xpipe.app.beacon.impl; + +import com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.beacon.AppBeaconServer; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.beacon.BeaconClientException; +import io.xpipe.beacon.BeaconServerException; +import io.xpipe.beacon.api.ShellStopExchange; +import lombok.SneakyThrows; + +import java.io.IOException; + +public class ShellStopExchangeImpl extends ShellStopExchange { + + @Override + @SneakyThrows + public Object handle(HttpExchange exchange, Request msg) throws IOException, BeaconClientException, BeaconServerException { + var e = DataStorage.get().getStoreEntryIfPresent(msg.getConnection()).orElseThrow(() -> new IllegalArgumentException("Unknown connection")); + var existing = AppBeaconServer.get().getShellSessions().stream().filter(beaconShellSession -> beaconShellSession.getEntry().equals(e)).findFirst(); + if (existing.isPresent()) { + existing.get().getControl().close(); + AppBeaconServer.get().getShellSessions().remove(existing.get()); + } + return Response.builder().build(); + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/TerminalLaunchExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/TerminalLaunchExchangeImpl.java new file mode 100644 index 00000000..097486bc --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/impl/TerminalLaunchExchangeImpl.java @@ -0,0 +1,17 @@ +package io.xpipe.app.beacon.impl; + +import com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.util.TerminalLauncherManager; +import io.xpipe.beacon.BeaconClientException; +import io.xpipe.beacon.BeaconServerException; +import io.xpipe.beacon.api.TerminalLaunchExchange; + +import java.io.IOException; + +public class TerminalLaunchExchangeImpl extends TerminalLaunchExchange { + @Override + public Object handle(HttpExchange exchange, Request msg) throws IOException, BeaconClientException, BeaconServerException { + var r = TerminalLauncherManager.performLaunch(msg.getRequest()); + return Response.builder().targetFile(r).build(); + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/TerminalWaitExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/TerminalWaitExchangeImpl.java new file mode 100644 index 00000000..2114dacb --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/impl/TerminalWaitExchangeImpl.java @@ -0,0 +1,17 @@ +package io.xpipe.app.beacon.impl; + +import com.sun.net.httpserver.HttpExchange; +import io.xpipe.app.util.TerminalLauncherManager; +import io.xpipe.beacon.BeaconClientException; +import io.xpipe.beacon.BeaconServerException; +import io.xpipe.beacon.api.TerminalWaitExchange; + +import java.io.IOException; + +public class TerminalWaitExchangeImpl extends TerminalWaitExchange { + @Override + public Object handle(HttpExchange exchange, Request msg) throws IOException, BeaconClientException, BeaconServerException { + TerminalLauncherManager.waitForCompletion(msg.getRequest()); + return Response.builder().build(); + } +} 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 988b8e55..553aae3d 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java @@ -10,7 +10,6 @@ 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.core.process.OsType; import javafx.beans.binding.Bindings; import javafx.collections.FXCollections; import javafx.geometry.Insets; @@ -183,13 +182,7 @@ public class BrowserTransferComp extends SimpleComp { event.consume(); }); struc.get().setOnDragDone(event -> { - // macOS does always report false here, which is unfortunate - if (!event.isAccepted() && !OsType.getLocal().equals(OsType.MACOS)) { - return; - } - - // Don't clear, it might be more convenient to keep the contents - // model.clear(); + model.clear(); event.consume(); }); }), diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserEntry.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserEntry.java index bcc4e940..d8e61799 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserEntry.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserEntry.java @@ -27,8 +27,9 @@ public class BrowserEntry { if (rawFileEntry == null) { return null; } + rawFileEntry = rawFileEntry.resolved(); - if (rawFileEntry.getKind() == FileKind.DIRECTORY) { + if (rawFileEntry.getKind() != FileKind.FILE) { return null; } @@ -45,6 +46,7 @@ public class BrowserEntry { if (rawFileEntry == null) { return null; } + rawFileEntry = rawFileEntry.resolved(); if (rawFileEntry.getKind() != FileKind.DIRECTORY) { return null; @@ -58,13 +60,14 @@ public class BrowserEntry { return null; } + public String getIcon() { if (fileType != null) { return fileType.getIcon(); } else if (directoryType != null) { return directoryType.getIcon(rawFileEntry, false); } else { - return rawFileEntry.getKind() == FileKind.DIRECTORY + return rawFileEntry != null && rawFileEntry.resolved().getKind() == FileKind.DIRECTORY ? "default_folder.svg" : "default_file.svg"; } diff --git a/app/src/main/java/io/xpipe/app/browser/file/LocalFileSystem.java b/app/src/main/java/io/xpipe/app/browser/file/LocalFileSystem.java index 2fe56c18..50650aad 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/LocalFileSystem.java +++ b/app/src/main/java/io/xpipe/app/browser/file/LocalFileSystem.java @@ -4,7 +4,6 @@ import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.LocalStore; -import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -19,13 +18,13 @@ public class LocalFileSystem { } } - public static FileSystem.FileEntry getLocalFileEntry(Path file) throws IOException { + public static FileSystem.FileEntry getLocalFileEntry(Path file) throws Exception { if (localFileSystem == null) { throw new IllegalStateException(); } return new FileSystem.FileEntry( - localFileSystem, + localFileSystem.open(), file.toString(), Files.getLastModifiedTime(file).toInstant(), Files.isHidden(file), 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 f6c07d05..64414f53 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 @@ -23,7 +23,7 @@ public abstract class BrowserSessionTab { this.browserModel = browserModel; this.entry = entry; this.name = DataStorage.get().getStoreDisplayName(entry.get()); - this.tooltip = DataStorage.get().getId(entry.getEntry()).toString(); + this.tooltip = DataStorage.get().getStorePath(entry.getEntry()).toString(); } public abstract Comp comp(); 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 99786962..b64fcac0 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 @@ -13,6 +13,7 @@ import javafx.scene.control.ScrollPane; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; +import java.util.ArrayList; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; @@ -67,7 +68,9 @@ public class ListBoxViewComp extends Comp> { // Clear cache of unused values cache.keySet().removeIf(t -> !all.contains(t)); - var newShown = shown.stream() + // Create copy to reduce chances of concurrent modification + var shownCopy = new ArrayList<>(shown); + var newShown = shownCopy.stream() .map(v -> { if (!cache.containsKey(v)) { var comp = compFunction.apply(v); @@ -80,6 +83,10 @@ public class ListBoxViewComp extends Comp> { .limit(limit) .toList(); + if (listView.getChildren().equals(newShown)) { + return; + } + for (int i = 0; i < newShown.size(); i++) { var r = newShown.get(i); r.pseudoClassStateChanged(ODD, false); @@ -87,10 +94,8 @@ public class ListBoxViewComp extends Comp> { r.pseudoClassStateChanged(i % 2 == 0 ? EVEN : ODD, true); } - if (!listView.getChildren().equals(newShown)) { - var d = new DerivedObservableList<>(listView.getChildren(), true); - d.setContent(newShown); - } + var d = new DerivedObservableList<>(listView.getChildren(), true); + d.setContent(newShown); }; if (asynchronous) { 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 f8469ba8..83e6ac94 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 @@ -42,7 +42,7 @@ public class MarkdownComp extends Comp> { } private String getHtml() { - return MarkdownHelper.toHtml(markdown.getValue(), htmlTransformation); + return MarkdownHelper.toHtml(markdown.getValue(), s -> s, htmlTransformation); } @SneakyThrows @@ -55,8 +55,8 @@ public class MarkdownComp extends Comp> { AppProperties.get().getDataDir().resolve("webview").toFile()); wv.setPageFill(Color.TRANSPARENT); var theme = AppPrefs.get() != null && AppPrefs.get().theme.getValue().isDark() - ? "web/github-markdown-dark.css" - : "web/github-markdown-light.css"; + ? "misc/github-markdown-dark.css" + : "misc/github-markdown-light.css"; var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, theme).orElseThrow(); wv.getEngine().setUserStyleSheetLocation(url.toString()); 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 d7090c60..abc5f00f 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 @@ -9,6 +9,7 @@ import io.xpipe.app.fxcomps.augment.Augment; import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.TooltipAugment; import io.xpipe.app.fxcomps.util.PlatformThread; +import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.update.UpdateAvailableAlert; import io.xpipe.app.update.XPipeDistributionType; import io.xpipe.app.util.Hyperlinks; @@ -147,9 +148,8 @@ public class SideMenuBarComp extends Comp> { } { - var shortcut = new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 2]); var b = new IconButtonComp("mdi2t-translate", () -> Hyperlinks.open(Hyperlinks.TRANSLATE)) - .tooltipKey("translate", shortcut) + .tooltipKey("translate") .apply(simpleBorders) .accessibleTextKey("translate"); b.apply(struc -> { @@ -158,6 +158,17 @@ public class SideMenuBarComp extends Comp> { vbox.getChildren().add(b.createRegion()); } + { + var b = new IconButtonComp("mdi2c-code-json", () -> Hyperlinks.open("http://localhost:" + AppPrefs.get().httpServerPort().getValue())) + .tooltipKey("api") + .apply(simpleBorders) + .accessibleTextKey("api"); + b.apply(struc -> { + AppFont.setSize(struc.get(), 2); + }); + vbox.getChildren().add(b.createRegion()); + } + { var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableAlert.showIfNeeded()) .tooltipKey("updateAvailableTooltip") 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 e765e784..210920b8 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 @@ -1,18 +1,19 @@ package io.xpipe.app.comp.base; import io.xpipe.app.comp.store.StoreSection; +import io.xpipe.app.comp.store.StoreViewState; import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.SimpleComp; +import io.xpipe.app.fxcomps.util.LabelGraphic; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.store.DataStore; - +import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.value.ObservableBooleanValue; +import javafx.beans.value.ObservableValue; import javafx.scene.layout.Region; - import lombok.AllArgsConstructor; import lombok.Setter; @@ -24,17 +25,19 @@ import java.util.function.Function; public class StoreToggleComp extends SimpleComp { private final String nameKey; + private final ObservableValue graphic; private final StoreSection section; private final BooleanProperty value; private final Consumer onChange; @Setter - private ObservableBooleanValue customVisibility = new SimpleBooleanProperty(true); + private ObservableValue customVisibility = new SimpleBooleanProperty(true); public static StoreToggleComp simpleToggle( - String nameKey, StoreSection section, Function initial, BiConsumer setter) { + String nameKey, ObservableValue graphic, StoreSection section, Function initial, BiConsumer setter) { return new StoreToggleComp( nameKey, + graphic, section, new SimpleBooleanProperty( initial.apply(section.getWrapper().getEntry().getStore().asNeeded())), @@ -43,27 +46,57 @@ public class StoreToggleComp extends SimpleComp { }); } + public static StoreToggleComp enableToggle( + String nameKey, StoreSection section, Function initial, BiConsumer setter) { + var val = new SimpleBooleanProperty(); + ObservableValue g = val.map(aBoolean -> aBoolean ? + new LabelGraphic.IconGraphic("mdi2c-circle-slice-8") : new LabelGraphic.IconGraphic("mdi2p-power")); + var t = new StoreToggleComp( + nameKey, + g, + section, + new SimpleBooleanProperty( + initial.apply(section.getWrapper().getEntry().getStore().asNeeded())), + v -> { + setter.accept(section.getWrapper().getEntry().getStore().asNeeded(), v); + }); + t.tooltipKey("enabled"); + t.value.subscribe((newValue) -> { + val.set(newValue); + }); + return t; + } + public static StoreToggleComp childrenToggle( - String nameKey, StoreSection section, Function initial, BiConsumer setter) { - return new StoreToggleComp( - nameKey, - section, - new SimpleBooleanProperty( - initial.apply(section.getWrapper().getEntry().getStore().asNeeded())), - v -> { - setter.accept(section.getWrapper().getEntry().getStore().asNeeded(), v); - }); + String nameKey, boolean graphic, StoreSection section, Function initial, BiConsumer setter) { + var val = new SimpleBooleanProperty(); + ObservableValue g = graphic ? val.map(aBoolean -> aBoolean ? + new LabelGraphic.IconGraphic("mdi2c-circle-slice-8") : new LabelGraphic.IconGraphic("mdi2c-circle-half-full")) : null; + var t = new StoreToggleComp(nameKey, g, section, + new SimpleBooleanProperty(initial.apply(section.getWrapper().getEntry().getStore().asNeeded())), v -> { + Platform.runLater(() -> { + setter.accept(section.getWrapper().getEntry().getStore().asNeeded(), v); + StoreViewState.get().toggleStoreListUpdate(); + }); + }); + t.tooltipKey("showAllChildren"); + t.value.subscribe((newValue) -> { + val.set(newValue); + }); + return t; } - public StoreToggleComp(String nameKey, StoreSection section, boolean initial, Consumer onChange) { + public StoreToggleComp(String nameKey, ObservableValue graphic, StoreSection section, boolean initial, Consumer onChange) { this.nameKey = nameKey; + this.graphic = graphic; this.section = section; this.value = new SimpleBooleanProperty(initial); this.onChange = onChange; } - public StoreToggleComp(String nameKey, StoreSection section, BooleanProperty initial, Consumer onChange) { + public StoreToggleComp(String nameKey, ObservableValue graphic, StoreSection section, BooleanProperty initial, Consumer onChange) { this.nameKey = nameKey; + this.graphic = graphic; this.section = section; this.value = initial; this.onChange = onChange; @@ -74,7 +107,7 @@ public class StoreToggleComp extends SimpleComp { var disable = section.getWrapper().getValidity().map(state -> state != DataStoreEntry.Validity.COMPLETE); var visible = Bindings.createBooleanBinding( () -> { - if (!this.customVisibility.get()) { + if (!this.customVisibility.getValue()) { return false; } @@ -83,7 +116,7 @@ public class StoreToggleComp extends SimpleComp { section.getWrapper().getValidity(), section.getShowDetails(), this.customVisibility); - var t = new ToggleSwitchComp(value, AppI18n.observable(nameKey)) + var t = new ToggleSwitchComp(value, AppI18n.observable(nameKey), graphic) .visible(visible) .disable(disable); value.addListener((observable, oldValue, newValue) -> { diff --git a/app/src/main/java/io/xpipe/app/comp/base/ToggleSwitchComp.java b/app/src/main/java/io/xpipe/app/comp/base/ToggleSwitchComp.java index 05bd5bea..94960c97 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ToggleSwitchComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ToggleSwitchComp.java @@ -1,25 +1,25 @@ package io.xpipe.app.comp.base; +import atlantafx.base.controls.ToggleSwitch; import io.xpipe.app.fxcomps.SimpleComp; +import io.xpipe.app.fxcomps.util.LabelGraphic; import io.xpipe.app.fxcomps.util.PlatformThread; - import javafx.beans.property.Property; import javafx.beans.value.ObservableValue; +import javafx.css.PseudoClass; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.Region; +import lombok.EqualsAndHashCode; +import lombok.Value; -import atlantafx.base.controls.ToggleSwitch; - +@Value +@EqualsAndHashCode(callSuper = true) public class ToggleSwitchComp extends SimpleComp { - private final Property selected; - private final ObservableValue name; - - public ToggleSwitchComp(Property selected, ObservableValue name) { - this.selected = selected; - this.name = name; - } + Property selected; + ObservableValue name; + ObservableValue graphic; @Override protected Region createSimple() { @@ -43,6 +43,10 @@ public class ToggleSwitchComp extends SimpleComp { if (name != null) { s.textProperty().bind(PlatformThread.sync(name)); } + if (graphic != null) { + s.graphicProperty().bind(PlatformThread.sync(graphic.map(labelGraphic -> labelGraphic.createGraphicNode()))); + s.pseudoClassStateChanged(PseudoClass.getPseudoClass("has-graphic"),true); + } return s; } } diff --git a/app/src/main/java/io/xpipe/app/comp/store/DenseStoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/store/DenseStoreEntryComp.java index b5953cc8..29a52a0a 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 @@ -103,7 +103,7 @@ public class DenseStoreEntryComp extends StoreEntryComp { grid.getColumnConstraints().addAll(infoCC, custom); var cr = content != null ? content.createRegion() : new Region(); - var bb = createButtonBar().createRegion(); + var bb = createButtonBar(); var controls = new HBox(cr, bb); controls.setFillHeight(true); controls.setAlignment(Pos.CENTER_RIGHT); 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 a95a95db..595e7bc5 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 @@ -51,7 +51,7 @@ public class StandardStoreEntryComp extends StoreEntryComp { var custom = new ColumnConstraints(0, customSize, customSize); custom.setHalignment(HPos.RIGHT); var cr = content != null ? content.createRegion() : new Region(); - var bb = createButtonBar().createRegion(); + var bb = createButtonBar(); var controls = new HBox(cr, bb); controls.setFillHeight(true); HBox.setHgrow(cr, Priority.ALWAYS); 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 7220f5e8..a9588494 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 @@ -286,6 +286,10 @@ public class StoreCreationComp extends DialogComp { if (ex instanceof ValidationException) { ErrorEvent.expected(ex); skippable.set(false); + } else if (ex instanceof StackOverflowError) { + // Cycles in connection graphs can fail hard but are expected + ErrorEvent.expected(ex); + skippable.set(false); } else { skippable.set(true); } 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 cdcb3dc2..cbb597a5 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,12 +40,16 @@ public class StoreCreationMenu { menu.getItems() .add(category( - "addCommand", "mdi2c-code-greater-than", DataStoreProvider.CreationCategory.COMMAND, "cmd")); + "addService", "mdi2c-cloud-braces", DataStoreProvider.CreationCategory.SERVICE, null)); menu.getItems() .add(category( "addTunnel", "mdi2v-vector-polyline-plus", DataStoreProvider.CreationCategory.TUNNEL, null)); + menu.getItems() + .add(category( + "addCommand", "mdi2c-code-greater-than", DataStoreProvider.CreationCategory.COMMAND, "cmd")); + menu.getItems() .add(category("addDatabase", "mdi2d-database-plus", DataStoreProvider.CreationCategory.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 da3ab6be..6cd55eba 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 @@ -1,5 +1,6 @@ package io.xpipe.app.comp.store; +import atlantafx.base.layout.InputGroup; import atlantafx.base.theme.Styles; import io.xpipe.app.comp.base.LoadingOverlayComp; import io.xpipe.app.core.*; @@ -9,8 +10,12 @@ import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.augment.GrowAugment; -import io.xpipe.app.fxcomps.impl.*; +import io.xpipe.app.fxcomps.impl.IconButtonComp; +import io.xpipe.app.fxcomps.impl.LabelComp; +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.PlatformThread; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; @@ -199,63 +204,65 @@ public abstract class StoreEntryComp extends SimpleComp { return stack; } - protected Comp createButtonBar() { - var list = new ArrayList>(); - for (var p : wrapper.getActionProviders().entrySet()) { - var actionProvider = p.getKey().getDataStoreCallSite(); - if (!actionProvider.isMajor(wrapper.getEntry().ref())) { - continue; - } + protected Region createButtonBar() { + var list = new DerivedObservableList<>(wrapper.getActionProviders(), false); + var buttons = list.mapped(actionProvider -> { + var button = buildButton(actionProvider); + return button != null ? button.createRegion() : null; + }).filtered(region -> region != null).getList(); - var def = p.getKey().getDefaultDataStoreCallSite(); - if (def != null && def.equals(wrapper.getDefaultActionProvider().getValue())) { - continue; - } + var ig = new InputGroup(); + Runnable update = () -> { + var l = new ArrayList(buttons); + var settingsButton = createSettingsButton().createRegion(); + l.add(settingsButton); + l.forEach(o -> o.getStyleClass().remove(Styles.FLAT)); + ig.getChildren().setAll(l); + }; + buttons.subscribe(update); + update.run(); + ig.setAlignment(Pos.CENTER_RIGHT); + ig.setPadding(new Insets(5)); + ig.getStyleClass().add("button-bar"); + return ig; + } - var button = - new IconButtonComp(actionProvider.getIcon(wrapper.getEntry().ref()), () -> { - ThreadHelper.runFailableAsync(() -> { - var action = actionProvider.createAction( - wrapper.getEntry().ref()); - action.execute(); - }); + private Comp buildButton(ActionProvider p) { + var leaf = p.getLeafDataStoreCallSite(); + var branch = p.getBranchDataStoreCallSite(); + var cs = leaf != null ? leaf : branch; + + if (cs == null || !cs.isMajor(wrapper.getEntry().ref())) { + return null; + } + + var button = + new IconButtonComp(cs.getIcon(wrapper.getEntry().ref()), leaf != null ? () -> { + ThreadHelper.runFailableAsync(() -> { + wrapper.runAction(leaf.createAction(wrapper.getEntry().ref()), leaf.showBusy()); }); - button.accessibleText( - actionProvider.getName(wrapper.getEntry().ref()).getValue()); - button.apply(new TooltipAugment<>( - actionProvider.getName(wrapper.getEntry().ref()), null)); - if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ONLY_SHOW_IF_ENABLED) { - button.hide(Bindings.not(p.getValue())); - } else if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ALWAYS_SHOW) { - button.disable(Bindings.not(p.getValue())); - } - list.add(button); + } : null); + if (branch != null) { + button.apply(new ContextMenuAugment<>(mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY,keyEvent -> false,() -> { + var cm = ContextMenuHelper.create(); + branch.getChildren().forEach(childProvider -> { + var menu = buildMenuItemForAction(childProvider); + if (menu != null) { + cm.getItems().add(menu); + } + }); + return cm; + })); } - - var settingsButton = createSettingsButton(); - list.add(settingsButton); - if (list.size() > 1) { - list.getFirst().styleClass(Styles.LEFT_PILL); - for (int i = 1; i < list.size() - 1; i++) { - list.get(i).styleClass(Styles.CENTER_PILL); - } - list.getLast().styleClass(Styles.RIGHT_PILL); - } - list.forEach(comp -> { - comp.apply(struc -> { - struc.get().getStyleClass().remove(Styles.FLAT); - }); - }); - return new HorizontalComp(list) - .apply(struc -> { - struc.get().setAlignment(Pos.CENTER_RIGHT); - struc.get().setPadding(new Insets(5)); - }) - .styleClass("button-bar"); + button.accessibleText( + cs.getName(wrapper.getEntry().ref()).getValue()); + button.apply(new TooltipAugment<>( + cs.getName(wrapper.getEntry().ref()), null)); + return button; } protected Comp createSettingsButton() { - var settingsButton = new IconButtonComp("mdi2d-dots-horizontal-circle-outline", () -> {}); + var settingsButton = new IconButtonComp("mdi2d-dots-horizontal-circle-outline", null); settingsButton.styleClass("settings"); settingsButton.accessibleText("More"); settingsButton.apply(new ContextMenuAugment<>( @@ -271,103 +278,21 @@ public abstract class StoreEntryComp extends SimpleComp { AppFont.normal(contextMenu.getStyleableNode()); var hasSep = false; - for (var p : wrapper.getActionProviders().entrySet()) { - var actionProvider = p.getKey().getDataStoreCallSite(); - if (actionProvider.isMajor(wrapper.getEntry().ref())) { + for (var p : wrapper.getActionProviders()) { + var item = buildMenuItemForAction(p); + if (item == null) { continue; } - if (actionProvider.isSystemAction() && !hasSep) { + if (p.getLeafDataStoreCallSite() != null && p.getLeafDataStoreCallSite().isSystemAction() && !hasSep) { if (contextMenu.getItems().size() > 0) { contextMenu.getItems().add(new SeparatorMenuItem()); } hasSep = true; } - var name = actionProvider.getName(wrapper.getEntry().ref()); - var icon = actionProvider.getIcon(wrapper.getEntry().ref()); - var item = actionProvider.canLinkTo() - ? new Menu(null, new FontIcon(icon)) - : new MenuItem(null, new FontIcon(icon)); - - var proRequired = p.getKey().getProFeatureId() != null - && !LicenseProvider.get() - .getFeature(p.getKey().getProFeatureId()) - .isSupported(); - if (proRequired) { - item.setDisable(true); - item.textProperty().bind(Bindings.createStringBinding(() -> name.getValue() + " (Pro)", name)); - } else { - item.textProperty().bind(name); - } - - Menu menu = actionProvider.canLinkTo() ? (Menu) item : null; - item.setOnAction(event -> { - if (menu != null && !event.getTarget().equals(menu)) { - return; - } - - if (menu != null && menu.isDisable()) { - return; - } - - contextMenu.hide(); - ThreadHelper.runFailableAsync(() -> { - var action = actionProvider.createAction(wrapper.getEntry().ref()); - action.execute(); - }); - }); - if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ONLY_SHOW_IF_ENABLED) { - item.visibleProperty().bind(p.getValue()); - } else if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ALWAYS_SHOW) { - item.disableProperty().bind(Bindings.not(p.getValue())); - } contextMenu.getItems().add(item); - - if (menu != null) { - var run = new MenuItem(null, new FontIcon("mdi2c-code-greater-than")); - run.textProperty().bind(AppI18n.observable("base.execute")); - run.setOnAction(event -> { - ThreadHelper.runFailableAsync(() -> { - p.getKey() - .getDataStoreCallSite() - .createAction(wrapper.getEntry().ref()) - .execute(); - }); - }); - menu.getItems().add(run); - - var sc = new MenuItem(null, new FontIcon("mdi2c-code-greater-than")); - var url = "xpipe://action/" + p.getKey().getId() + "/" - + wrapper.getEntry().getUuid(); - sc.textProperty().bind(AppI18n.observable("base.createShortcut")); - sc.setOnAction(event -> { - ThreadHelper.runFailableAsync(() -> { - DesktopShortcuts.create( - url, - wrapper.nameProperty().getValue() + " (" - + p.getKey() - .getDataStoreCallSite() - .getName(wrapper.getEntry().ref()) - .getValue() + ")"); - }); - }); - menu.getItems().add(sc); - - if (XPipeDistributionType.get().isSupportsUrls()) { - var l = new MenuItem(null, new FontIcon("mdi2c-clipboard-list-outline")); - l.textProperty().bind(AppI18n.observable("base.copyShareLink")); - l.setOnAction(event -> { - ThreadHelper.runFailableAsync(() -> { - AppActionLinkDetector.setLastDetectedAction(url); - ClipboardHelper.copyUrl(url); - }); - }); - menu.getItems().add(l); - } - } } - if (contextMenu.getItems().size() > 0 && !hasSep) { contextMenu.getItems().add(new SeparatorMenuItem()); } @@ -385,6 +310,11 @@ public abstract class StoreEntryComp extends SimpleComp { browse.setOnAction( event -> DesktopHelper.browsePathLocal(wrapper.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())); + contextMenu.getItems().add(copyId); } if (DataStorage.get().isRootEntry(wrapper.getEntry())) { @@ -418,7 +348,7 @@ public abstract class StoreEntryComp extends SimpleComp { wrapper.moveTo(storeCategoryWrapper.getCategory()); event.consume(); }); - if (storeCategoryWrapper.getParent() == null) { + if (storeCategoryWrapper.getParent() == null || storeCategoryWrapper.equals(wrapper.getCategory().getValue())) { m.setDisable(true); } @@ -490,6 +420,94 @@ public abstract class StoreEntryComp extends SimpleComp { return contextMenu; } + private MenuItem buildMenuItemForAction(ActionProvider p) { + var leaf = p.getLeafDataStoreCallSite(); + var branch = p.getBranchDataStoreCallSite(); + var cs = leaf != null ? leaf : branch; + + if (cs == null || cs.isMajor(wrapper.getEntry().ref())) { + return null; + } + + var name = cs.getName(wrapper.getEntry().ref()); + var icon = cs.getIcon(wrapper.getEntry().ref()); + var item = (leaf != null && leaf.canLinkTo()) || branch != null + ? new Menu(null, new FontIcon(icon)) + : new MenuItem(null, new FontIcon(icon)); + + var proRequired = p.getProFeatureId() != null + && !LicenseProvider.get() + .getFeature(p.getProFeatureId()) + .isSupported(); + if (proRequired) { + item.setDisable(true); + item.textProperty().bind(Bindings.createStringBinding(() -> name.getValue() + " (Pro)", name)); + } else { + item.textProperty().bind(name); + } + Menu menu = item instanceof Menu m ? m : null; + + if (branch != null) { + var items = branch.getChildren().stream().map(c -> buildMenuItemForAction(c)).toList(); + menu.getItems().addAll(items); + return menu; + } else if (leaf.canLinkTo()) { + var run = new MenuItem(null, new FontIcon("mdi2c-code-greater-than")); + run.textProperty().bind(AppI18n.observable("base.execute")); + run.setOnAction(event -> { + ThreadHelper.runFailableAsync(() -> { + wrapper.runAction(leaf.createAction(wrapper.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(); + sc.textProperty().bind(AppI18n.observable("base.createShortcut")); + sc.setOnAction(event -> { + ThreadHelper.runFailableAsync(() -> { + DesktopShortcuts.create( + url, + wrapper.nameProperty().getValue() + " (" + + p + .getLeafDataStoreCallSite() + .getName(wrapper.getEntry().ref()) + .getValue() + ")"); + }); + }); + menu.getItems().add(sc); + + if (XPipeDistributionType.get().isSupportsUrls()) { + var l = new MenuItem(null, new FontIcon("mdi2c-clipboard-list-outline")); + l.textProperty().bind(AppI18n.observable("base.copyShareLink")); + l.setOnAction(event -> { + ThreadHelper.runFailableAsync(() -> { + AppActionLinkDetector.setLastDetectedAction(url); + ClipboardHelper.copyUrl(url); + }); + }); + menu.getItems().add(l); + } + } + + item.setOnAction(event -> { + if (menu != null && !event.getTarget().equals(menu)) { + return; + } + + if (menu != null && menu.isDisable()) { + return; + } + + event.consume(); + ThreadHelper.runFailableAsync(() -> { + wrapper.runAction(leaf.createAction(wrapper.getEntry().ref()), leaf.showBusy()); + }); + }); + + return item; + } private static String DEFAULT_NOTES = null; diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListStatusComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListStatusComp.java index f657af52..a1111478 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListStatusComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListStatusComp.java @@ -61,14 +61,14 @@ public class StoreEntryListStatusComp extends SimpleComp { var inRootCategory = StoreViewState.get().getActiveCategory().getValue().getRoot().equals(rootCategory); // Sadly the all binding does not update when the individual visibility of entries changes // But it is good enough. - var showProvider = storeEntryWrapper.getEntry().getProvider() == null || + var showProvider = !storeEntryWrapper.getEntry().getValidity().isUsable() || storeEntryWrapper.getEntry().getProvider().shouldShow(storeEntryWrapper); return inRootCategory && showProvider; }, StoreViewState.get().getActiveCategory()); var shownList = all.filtered( storeEntryWrapper -> { - return storeEntryWrapper.shouldShow( + return storeEntryWrapper.matchesFilter( StoreViewState.get().getFilterString().getValue()); }, StoreViewState.get().getFilterString()); 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 5d96ed2d..c548f5fd 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 @@ -9,8 +9,8 @@ import io.xpipe.app.storage.DataStoreCategory; import io.xpipe.app.storage.DataStoreColor; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.util.ThreadHelper; -import javafx.beans.Observable; import javafx.beans.property.*; +import javafx.collections.FXCollections; import lombok.Getter; import java.time.Duration; @@ -26,8 +26,8 @@ public class StoreEntryWrapper { private final BooleanProperty disabled = new SimpleBooleanProperty(); private final BooleanProperty busy = new SimpleBooleanProperty(); private final Property validity = new SimpleObjectProperty<>(); - private final Map actionProviders; - private final Property> defaultActionProvider; + private final ListProperty actionProviders = new SimpleListProperty<>(FXCollections.observableArrayList()); + private final Property defaultActionProvider = new SimpleObjectProperty<>(); private final BooleanProperty deletable = new SimpleBooleanProperty(); private final BooleanProperty expanded = new SimpleBooleanProperty(); private final Property persistentState = new SimpleObjectProperty<>(); @@ -41,30 +41,24 @@ public class StoreEntryWrapper { this.entry = entry; this.name = new SimpleStringProperty(entry.getName()); this.lastAccess = new SimpleObjectProperty<>(entry.getLastAccess().minus(Duration.ofMillis(500))); - this.actionProviders = new LinkedHashMap<>(); ActionProvider.ALL.stream() .filter(dataStoreActionProvider -> { return !entry.isDisabled() - && dataStoreActionProvider.getDataStoreCallSite() != null + && dataStoreActionProvider.getLeafDataStoreCallSite() != null && dataStoreActionProvider - .getDataStoreCallSite() + .getLeafDataStoreCallSite() .getApplicableClass() .isAssignableFrom(entry.getStore().getClass()); }) .sorted(Comparator.comparing( - actionProvider -> actionProvider.getDataStoreCallSite().isSystemAction())) + actionProvider -> actionProvider.getLeafDataStoreCallSite().isSystemAction())) .forEach(dataStoreActionProvider -> { - actionProviders.put(dataStoreActionProvider, new SimpleBooleanProperty(true)); + actionProviders.add(dataStoreActionProvider); }); - this.defaultActionProvider = new SimpleObjectProperty<>(); this.notes = new SimpleObjectProperty<>(new StoreNotes(entry.getNotes(), entry.getNotes())); setupListeners(); } - public List getUpdateObservables() { - return List.of(category); - } - public void moveTo(DataStoreCategory category) { ThreadHelper.runAsync(() -> { DataStorage.get().updateCategory(entry, category); @@ -136,7 +130,7 @@ public class StoreEntryWrapper { color.setValue(entry.getColor()); notes.setValue(new StoreNotes(entry.getNotes(), entry.getNotes())); - busy.setValue(entry.isInRefresh()); + busy.setValue(entry.getBusyCounter().get() != 0); deletable.setValue(entry.getConfiguration().isDeletable() || AppPrefs.get().developerDisableGuiRestrictions().getValue()); @@ -156,48 +150,56 @@ public class StoreEntryWrapper { } } - actionProviders.keySet().forEach(dataStoreActionProvider -> { - if (!isInStorage()) { - actionProviders.get(dataStoreActionProvider).set(false); - defaultActionProvider.setValue(null); - return; - } - - if (!entry.getValidity().isUsable() - && !dataStoreActionProvider - .getDataStoreCallSite() - .activeType() - .equals(ActionProvider.DataStoreCallSite.ActiveType.ALWAYS_ENABLE)) { - actionProviders.get(dataStoreActionProvider).set(false); - return; - } - + if (!isInStorage()) { + actionProviders.clear(); + defaultActionProvider.setValue(null); + } else { var defaultProvider = ActionProvider.ALL.stream() - .filter(e -> e.getDefaultDataStoreCallSite() != null + .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() - .map(ActionProvider::getDefaultDataStoreCallSite) .orElse(null); this.defaultActionProvider.setValue(defaultProvider); try { - actionProviders - .get(dataStoreActionProvider) - .set(dataStoreActionProvider - .getDataStoreCallSite() - .getApplicableClass() - .isAssignableFrom(entry.getStore().getClass()) - && dataStoreActionProvider - .getDataStoreCallSite() - .isApplicable(entry.ref())); + var newProviders = ActionProvider.ALL.stream() + .filter(dataStoreActionProvider -> { + return showActionProvider(dataStoreActionProvider); + }) + .sorted(Comparator.comparing( + actionProvider -> actionProvider.getLeafDataStoreCallSite() != null && + actionProvider.getLeafDataStoreCallSite().isSystemAction())) + .toList(); + if (!actionProviders.equals(newProviders)) { + actionProviders.setAll(newProviders); + } } catch (Exception ex) { ErrorEvent.fromThrowable(ex).handle(); - actionProviders.get(dataStoreActionProvider).set(false); } - }); + } + } + + private boolean showActionProvider(ActionProvider p) { + var leaf = p.getLeafDataStoreCallSite(); + if (leaf != null) { + return (entry.getValidity().isUsable() || (!leaf.requiresValidStore() && entry.getProvider() != null)) + && leaf.getApplicableClass().isAssignableFrom(entry.getStore().getClass()) + && leaf + .isApplicable(entry.ref()); + } + + + var branch = p.getBranchDataStoreCallSite(); + if (branch != null && entry.getStore() != null && branch.getApplicableClass().isAssignableFrom(entry.getStore().getClass())) { + return branch.getChildren().stream().anyMatch(child -> { + return showActionProvider(child); + }); + } + + return false; } public void refreshChildren() { @@ -220,17 +222,31 @@ public class StoreEntryWrapper { var found = getDefaultActionProvider().getValue(); entry.notifyUpdate(true, false); if (found != null) { - found.createAction(entry.ref()).execute(); + var act = found.getDefaultDataStoreCallSite().createAction(entry.ref()); + runAction(act,found.getDefaultDataStoreCallSite().showBusy()); } else { entry.setExpanded(!entry.isExpanded()); } } + public void runAction(ActionProvider.Action action, boolean showBusy) throws Exception { + try { + if (showBusy) { + getEntry().incrementBusyCounter(); + } + action.execute(); + } finally { + if (showBusy) { + getEntry().decrementBusyCounter(); + } + } + } + public void toggleExpanded() { this.expanded.set(!expanded.getValue()); } - public boolean shouldShow(String filter) { + public boolean matchesFilter(String filter) { if (filter == null || nameProperty().getValue().toLowerCase().contains(filter.toLowerCase())) { return true; } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreIntroComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreIntroComp.java index 7b3602c2..63ba318f 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreIntroComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreIntroComp.java @@ -1,13 +1,15 @@ package io.xpipe.app.comp.store; +import atlantafx.base.theme.Styles; import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.impl.PrettySvgComp; +import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.util.ScanAlert; - import javafx.beans.property.SimpleStringProperty; +import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; @@ -15,14 +17,11 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; - -import atlantafx.base.theme.Styles; import org.kordamp.ikonli.javafx.FontIcon; public class StoreIntroComp extends SimpleComp { - @Override - public Region createSimple() { + private Region createIntro() { var title = new Label(); title.textProperty().bind(AppI18n.observable("storeIntroTitle")); title.getStyleClass().add(Styles.TEXT_BOLD); @@ -45,7 +44,7 @@ public class StoreIntroComp extends SimpleComp { text.setSpacing(5); text.setAlignment(Pos.CENTER_LEFT); var hbox = new HBox(img, text); - hbox.setSpacing(35); + hbox.setSpacing(55); hbox.setAlignment(Pos.CENTER); var v = new VBox(hbox, scanPane); @@ -56,8 +55,63 @@ public class StoreIntroComp extends SimpleComp { v.setSpacing(10); v.getStyleClass().add("intro"); + return v; + } + + + private Region createImportIntro() { + var title = new Label(); + title.textProperty().bind(AppI18n.observable("importConnectionsTitle")); + title.getStyleClass().add(Styles.TEXT_BOLD); + AppFont.setSize(title, 7); + + var importDesc = new Label(); + importDesc.textProperty().bind(AppI18n.observable("storeIntroImportDescription")); + importDesc.setWrapText(true); + importDesc.setMaxWidth(470); + + var importButton = new Button(null, new FontIcon("mdi2g-git")); + importButton.textProperty().bind(AppI18n.observable("importConnections")); + importButton.setOnAction(event -> AppPrefs.get().selectCategory("sync")); + var importPane = new StackPane(importButton); + importPane.setAlignment(Pos.CENTER); + + var fi = new FontIcon("mdi2g-git"); + fi.setIconSize(80); + var img = new StackPane(fi); + img.setPrefWidth(100); + img.setPrefHeight(150); + var text = new VBox(title, importDesc); + text.setSpacing(5); + text.setAlignment(Pos.CENTER_LEFT); + var hbox = new HBox(img, text); + hbox.setSpacing(35); + hbox.setAlignment(Pos.CENTER); + + var v = new VBox(hbox, importPane); + v.setMinWidth(Region.USE_PREF_SIZE); + v.setMaxWidth(Region.USE_PREF_SIZE); + v.setMinHeight(Region.USE_PREF_SIZE); + v.setMaxHeight(Region.USE_PREF_SIZE); + + v.setSpacing(10); + v.getStyleClass().add("intro"); + return v; + } + + @Override + public Region createSimple() { + var intro = createIntro(); + var introImport = createImportIntro(); + var v = new VBox(intro, introImport); + v.setSpacing(80); + v.setMinWidth(Region.USE_PREF_SIZE); + v.setMaxWidth(Region.USE_PREF_SIZE); + v.setMinHeight(Region.USE_PREF_SIZE); + v.setMaxHeight(Region.USE_PREF_SIZE); var sp = new StackPane(v); + sp.setPadding(new Insets(40, 0, 0, 0)); sp.setAlignment(Pos.CENTER); sp.setPickOnBounds(false); return sp; 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 67bb0cda..fe7e19ae 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 @@ -14,9 +14,7 @@ import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import lombok.Value; -import java.util.Comparator; -import java.util.HashSet; -import java.util.Set; +import java.util.*; import java.util.function.Predicate; import java.util.function.ToIntFunction; @@ -87,7 +85,7 @@ public class StoreSection { } seen.add(wrapper); - var found = list.getList().stream().filter(section -> wrapper.getEntry().getOrderBefore().equals(section.getWrapper().getEntry().getUuid())).findFirst(); + var found = list.getList().stream().filter(section -> section.getWrapper().getEntry().getUuid().equals(wrapper.getEntry().getOrderBefore())).findFirst(); if (found.isPresent()) { return count(found.get().getWrapper(), seen); } else { @@ -125,16 +123,16 @@ public class StoreSection { category, StoreViewState.get().getEntriesListChangeObservable()); var cached = topLevel.mapped( - storeEntryWrapper -> create(storeEntryWrapper, 1, all, entryFilter, filterString, category)); + storeEntryWrapper -> create(List.of(), storeEntryWrapper, 1, all, entryFilter, filterString, category)); var ordered = sorted(cached, category); var shown = ordered.filtered( section -> { - var showFilter = filterString == null || section.matchesFilter(filterString.get()); - var matchesSelector = section.anyMatches(entryFilter); - var sameCategory = category == null - || category.getValue() == null - || showInCategory(category.getValue(), section.getWrapper()); - return showFilter && matchesSelector && sameCategory; + // matches filter + return (filterString == null || section.matchesFilter(filterString.get())) && + // matches selector + (section.anyMatches(entryFilter)) && + // same category + (category == null || category.getValue() == null || showInCategory(category.getValue(), section.getWrapper())); }, category, filterString); @@ -142,6 +140,7 @@ public class StoreSection { } private static StoreSection create( + List parents, StoreEntryWrapper e, int depth, DerivedObservableList all, @@ -161,31 +160,28 @@ public class StoreSection { // .map(found -> found.equals(e.getEntry())) // .orElse(false); - // This check is fast as the children are cached in the storage - var isChildren = DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry()); - var showProvider = other.getEntry().getProvider() == null || - other.getEntry().getProvider().shouldShow(other); - return isChildren && showProvider; + // is children. This check is fast as the children are cached in the storage + return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry()) && + // show provider + (!other.getEntry().getValidity().isUsable() || other.getEntry().getProvider().shouldShow(other)); }, e.getPersistentState(), e.getCache(), StoreViewState.get().getEntriesListChangeObservable()); - var cached = allChildren.mapped( - entry1 -> create(entry1, depth + 1, all, entryFilter, filterString, category)); + var l = new ArrayList<>(parents); + l.add(e); + var cached = allChildren.mapped(c -> create(l, c, depth + 1, all, entryFilter, filterString, category)); var ordered = sorted(cached, category); var filtered = ordered.filtered( section -> { - var showFilter = filterString == null || section.matchesFilter(filterString.get()); - var matchesSelector = section.anyMatches(entryFilter); - // Prevent updates for children on category switching by checking depth - var showCategory = category == null - || category.getValue() == null - || showInCategory(category.getValue(), section.getWrapper()) - || depth > 0; - // If this entry is already shown as root due to a different category than parent, don't show it - // again here - var notRoot = + // matches filter + return (filterString == null || section.matchesFilter(filterString.get()) || l.stream().anyMatch(p -> p.matchesFilter(filterString.get()))) && + // matches selector + section.anyMatches(entryFilter) && + // matches category + // Prevent updates for children on category switching by checking depth + (category == null || category.getValue() == null || showInCategory(category.getValue(), section.getWrapper()) || depth > 0) && + // not root + // If this entry is already shown as root due to a different category than parent, don't show it + // again here !DataStorage.get().isRootEntry(section.getWrapper().getEntry()); - var showProvider = section.getWrapper().getEntry().getProvider() == null || - section.getWrapper().getEntry().getProvider().shouldShow(section.getWrapper()); - return showFilter && matchesSelector && showCategory && notRoot && showProvider; }, category, filterString, @@ -214,7 +210,7 @@ public class StoreSection { } public boolean matchesFilter(String filter) { - return anyMatches(storeEntryWrapper -> storeEntryWrapper.shouldShow(filter)); + return anyMatches(storeEntryWrapper -> storeEntryWrapper.matchesFilter(filter)); } public boolean anyMatches(Predicate c) { 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 238cfe0d..b1792dcf 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 @@ -2,6 +2,7 @@ package io.xpipe.app.comp.store; import io.xpipe.app.core.AppCache; import io.xpipe.app.fxcomps.util.DerivedObservableList; +import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; @@ -117,6 +118,18 @@ public class StoreViewState { .orElseThrow())); } + public void toggleStoreOrderUpdate() { + PlatformThread.runLaterIfNeeded(() -> { + entriesOrderChangeObservable.set(entriesOrderChangeObservable.get() + 1); + }); + } + + public void toggleStoreListUpdate() { + PlatformThread.runLaterIfNeeded(() -> { + entriesListChangeObservable.set(entriesListChangeObservable.get() + 1); + }); + } + private void addListeners() { if (AppPrefs.get() != null) { AppPrefs.get().condenseConnectionDisplay().addListener((observable, oldValue, newValue) -> { @@ -136,14 +149,14 @@ public class StoreViewState { @Override public void onStoreOrderUpdate() { Platform.runLater(() -> { - entriesOrderChangeObservable.set(entriesOrderChangeObservable.get() + 1); + toggleStoreOrderUpdate(); }); } @Override public void onStoreListUpdate() { Platform.runLater(() -> { - entriesListChangeObservable.set(entriesListChangeObservable.get() + 1); + toggleStoreListUpdate(); }); } @@ -281,11 +294,9 @@ public class StoreViewState { public int compare(StoreCategoryWrapper o1, StoreCategoryWrapper o2) { var o1Root = o1.getRoot(); var o2Root = o2.getRoot(); - if (o1Root.equals(getAllConnectionsCategory()) && !o1Root.equals(o2Root)) { return -1; } - if (o2Root.equals(getAllConnectionsCategory()) && !o1Root.equals(o2Root)) { return 1; } @@ -302,6 +313,22 @@ public class StoreViewState { return 1; } + if (o1.getDepth() > o2.getDepth()) { + if (o1.getParent() == o2) { + return 1; + } + + return compare(o1.getParent(), o2); + } + + if (o1.getDepth() < o2.getDepth()) { + if (o2.getParent() == o1) { + return -1; + } + + return compare(o1, o2.getParent()); + } + var parent = compare(o1.getParent(), o2.getParent()); if (parent != 0) { return parent; diff --git a/app/src/main/java/io/xpipe/app/core/AppDesktopIntegration.java b/app/src/main/java/io/xpipe/app/core/AppDesktopIntegration.java index c6900b53..5ad9b00e 100644 --- a/app/src/main/java/io/xpipe/app/core/AppDesktopIntegration.java +++ b/app/src/main/java/io/xpipe/app/core/AppDesktopIntegration.java @@ -5,6 +5,7 @@ import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.launcher.LauncherInput; import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.util.PlatformState; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.process.OsType; @@ -16,6 +17,10 @@ import java.util.List; public class AppDesktopIntegration { public static void setupDesktopIntegrations() { + if (PlatformState.getCurrent() != PlatformState.RUNNING) { + return; + } + try { if (Desktop.isDesktopSupported()) { Desktop.getDesktop().addAppEventListener(new SystemSleepListener() { 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 9f7de75d..b3238678 100644 --- a/app/src/main/java/io/xpipe/app/core/AppExtensionManager.java +++ b/app/src/main/java/io/xpipe/app/core/AppExtensionManager.java @@ -72,7 +72,7 @@ public class AppExtensionManager { private void loadBaseExtension() { var baseModule = findAndParseExtension("base", ModuleLayer.boot()); if (baseModule.isEmpty()) { - throw new ExtensionException("Missing base module. Is the installation corrupt?"); + throw new ExtensionException("Missing base module. Is the installation data corrupt?"); } baseLayer = baseModule.get().getModule().getLayer(); 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 bb173590..700c71dd 100644 --- a/app/src/main/java/io/xpipe/app/core/AppProperties.java +++ b/app/src/main/java/io/xpipe/app/core/AppProperties.java @@ -50,11 +50,10 @@ public class AppProperties { Properties props = new Properties(); props.load(Files.newInputStream(propsFile)); props.forEach((key, value) -> { - if (System.getProperty(key.toString()) != null) { - return; + // Don't overwrite existing properties + if (System.getProperty(key.toString()) == null) { + System.setProperty(key.toString(), value.toString()); } - - System.setProperty(key.toString(), value.toString()); }); } catch (IOException e) { ErrorEvent.fromThrowable(e).handle(); diff --git a/app/src/main/java/io/xpipe/app/core/AppSocketServer.java b/app/src/main/java/io/xpipe/app/core/AppSocketServer.java deleted file mode 100644 index eb2ac169..00000000 --- a/app/src/main/java/io/xpipe/app/core/AppSocketServer.java +++ /dev/null @@ -1,358 +0,0 @@ -package io.xpipe.app.core; - -import io.xpipe.app.exchange.MessageExchangeImpls; -import io.xpipe.app.issue.ErrorEvent; -import io.xpipe.app.issue.TrackEvent; -import io.xpipe.beacon.*; -import io.xpipe.beacon.exchange.MessageExchanges; -import io.xpipe.beacon.exchange.data.ClientErrorMessage; -import io.xpipe.beacon.exchange.data.ServerErrorMessage; -import io.xpipe.core.util.Deobfuscator; -import io.xpipe.core.util.FailableRunnable; -import io.xpipe.core.util.JacksonMapper; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.StringWriter; -import java.net.InetAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.SocketException; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.HexFormat; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicReference; - -public class AppSocketServer { - - private static AppSocketServer INSTANCE; - private final int port; - private ServerSocket socket; - private boolean running; - private int connectionCounter; - private Thread listenerThread; - - private AppSocketServer(int port) { - this.port = port; - } - - public static void init() { - int port = -1; - try { - port = BeaconConfig.getUsedPort(); - INSTANCE = new AppSocketServer(port); - INSTANCE.createSocketListener(); - - TrackEvent.withInfo("Initialized socket server") - .tag("port", port) - .build() - .handle(); - } catch (Exception ex) { - // Not terminal! - ErrorEvent.fromThrowable(ex) - .description("Unable to start local socket server on port " + port) - .build() - .handle(); - } - } - - public static void reset() { - if (INSTANCE != null) { - INSTANCE.stop(); - INSTANCE = null; - } - } - - private void stop() { - if (!running) { - return; - } - - running = false; - try { - socket.close(); - } catch (IOException e) { - ErrorEvent.fromThrowable(e).handle(); - } - try { - listenerThread.join(); - } catch (InterruptedException ignored) { - } - } - - private void createSocketListener() throws IOException { - socket = new ServerSocket(port, 10000, InetAddress.getLoopbackAddress()); - running = true; - listenerThread = new Thread( - () -> { - while (running) { - Socket clientSocket; - try { - clientSocket = socket.accept(); - } catch (Exception ex) { - continue; - } - - try { - performExchangesAsync(clientSocket); - } catch (Exception ex) { - ErrorEvent.fromThrowable(ex).build().handle(); - } - connectionCounter++; - } - }, - "socket server"); - listenerThread.start(); - } - - private boolean performExchange(Socket clientSocket, int id) throws Exception { - if (clientSocket.isClosed()) { - TrackEvent.trace("Socket closed"); - return false; - } - - JsonNode node; - try (InputStream blockIn = BeaconFormat.readBlocks(clientSocket.getInputStream())) { - node = JacksonMapper.getDefault().readTree(blockIn); - } - if (node.isMissingNode()) { - TrackEvent.trace("Received EOF"); - return false; - } - - TrackEvent.trace("Received raw request: \n" + node.toPrettyString()); - - var req = parseRequest(node); - TrackEvent.trace("Parsed request: \n" + req.toString()); - - var prov = MessageExchangeImpls.byRequest(req); - if (prov.isEmpty()) { - throw new IllegalArgumentException("Unknown request id: " + req.getClass()); - } - AtomicReference> post = new AtomicReference<>(); - var res = prov.get() - .handleRequest( - new BeaconHandler() { - @Override - public void postResponse(FailableRunnable r) { - post.set(r); - } - - @Override - public OutputStream sendBody() throws IOException { - TrackEvent.trace("Starting writing body for #" + id); - return AppSocketServer.this.sendBody(clientSocket); - } - - @Override - public InputStream receiveBody() throws IOException { - TrackEvent.trace("Starting to read body for #" + id); - return AppSocketServer.this.receiveBody(clientSocket); - } - }, - req); - - TrackEvent.trace("Sending response to #" + id + ": \n" + res.toString()); - AppSocketServer.this.sendResponse(clientSocket, res); - - try { - // If this fails, we sadly can't send an error response. Therefore just report it on the server side - if (post.get() != null) { - post.get().run(); - } - } catch (Exception ex) { - ErrorEvent.fromThrowable(ex).handle(); - } - - TrackEvent.builder() - .type("trace") - .message("Socket connection #" + id + " performed exchange " - + req.getClass().getSimpleName()) - .build() - .handle(); - - return true; - } - - private void performExchanges(Socket clientSocket, int id) { - try { - JsonNode informationNode; - try (InputStream blockIn = BeaconFormat.readBlocks(clientSocket.getInputStream())) { - informationNode = JacksonMapper.getDefault().readTree(blockIn); - } - if (informationNode.isMissingNode()) { - TrackEvent.trace("Received EOF"); - return; - } - var information = - JacksonMapper.getDefault().treeToValue(informationNode, BeaconClient.ClientInformation.class); - try (var blockOut = BeaconFormat.writeBlocks(clientSocket.getOutputStream())) { - blockOut.write("\"ACK\"".getBytes(StandardCharsets.UTF_8)); - } - - TrackEvent.builder() - .type("trace") - .message("Created new socket connection #" + id) - .tag("client", information != null ? information.toDisplayString() : "Unknown") - .build() - .handle(); - - try { - while (true) { - if (!performExchange(clientSocket, id)) { - break; - } - } - TrackEvent.builder() - .type("trace") - .message("Socket connection #" + id + " finished successfully") - .build() - .handle(); - - } catch (ClientException ce) { - TrackEvent.trace("Sending client error to #" + id + ": " + ce.getMessage()); - sendClientErrorResponse(clientSocket, ce.getMessage()); - } catch (ServerException se) { - TrackEvent.trace("Sending server error to #" + id + ": " + se.getMessage()); - Deobfuscator.deobfuscate(se); - sendServerErrorResponse(clientSocket, se); - var toReport = se.getCause() != null ? se.getCause() : se; - ErrorEvent.fromThrowable(toReport).build().handle(); - } catch (SocketException ex) { - // Do not send error and omit it, as this might happen often - // This is expected if you kill a running xpipe CLI process - // We do not send the error to the client as the socket connection might be broken - ErrorEvent.fromThrowable(ex).omitted(true).expected().build().handle(); - } catch (Throwable ex) { - TrackEvent.trace("Sending internal server error to #" + id + ": " + ex.getMessage()); - Deobfuscator.deobfuscate(ex); - sendServerErrorResponse(clientSocket, ex); - ErrorEvent.fromThrowable(ex).build().handle(); - } - } catch (SocketException ex) { - // Omit it, as this might happen often - // This is expected if you kill a running xpipe CLI process - ErrorEvent.fromThrowable(ex).expected().omit().build().handle(); - } catch (Throwable ex) { - ErrorEvent.fromThrowable(ex).build().handle(); - } finally { - try { - clientSocket.close(); - TrackEvent.trace("Closed socket #" + id); - } catch (IOException e) { - ErrorEvent.fromThrowable(e).build().handle(); - } - } - - TrackEvent.builder().type("trace").message("Socket connection #" + id + " finished unsuccessfully"); - } - - private void performExchangesAsync(Socket clientSocket) { - var id = connectionCounter; - var t = new Thread( - () -> { - performExchanges(clientSocket, id); - }, - "socket connection #" + id); - t.start(); - } - - public OutputStream sendBody(Socket outSocket) throws IOException { - outSocket.getOutputStream().write(BeaconConfig.BODY_SEPARATOR); - return BeaconFormat.writeBlocks(outSocket.getOutputStream()); - } - - public InputStream receiveBody(Socket outSocket) throws IOException { - var read = outSocket.getInputStream().readNBytes(BeaconConfig.BODY_SEPARATOR.length); - if (!Arrays.equals(read, BeaconConfig.BODY_SEPARATOR)) { - throw new IOException("Expected body start (" + HexFormat.of().formatHex(BeaconConfig.BODY_SEPARATOR) - + ") but got " + HexFormat.of().formatHex(read)); - } - return BeaconFormat.readBlocks(outSocket.getInputStream()); - } - - public void sendResponse(Socket outSocket, T obj) throws Exception { - ObjectNode json = JacksonMapper.getDefault().valueToTree(obj); - var prov = MessageExchanges.byResponse(obj).get(); - json.set("messageType", new TextNode(prov.getId())); - json.set("messagePhase", new TextNode("response")); - var msg = JsonNodeFactory.instance.objectNode(); - msg.set("xPipeMessage", json); - - var writer = new StringWriter(); - var mapper = JacksonMapper.getDefault(); - try (JsonGenerator g = mapper.createGenerator(writer).setPrettyPrinter(new DefaultPrettyPrinter())) { - g.writeTree(msg); - } catch (IOException ex) { - throw new ConnectorException("Couldn't serialize request", ex); - } - - var content = writer.toString(); - TrackEvent.trace("Sending raw response:\n" + content); - try (OutputStream blockOut = BeaconFormat.writeBlocks(outSocket.getOutputStream())) { - blockOut.write(content.getBytes(StandardCharsets.UTF_8)); - } - } - - public void sendClientErrorResponse(Socket outSocket, String message) throws Exception { - var err = new ClientErrorMessage(message); - ObjectNode json = JacksonMapper.getDefault().valueToTree(err); - var msg = JsonNodeFactory.instance.objectNode(); - msg.set("xPipeClientError", json); - - // Don't log this as it clutters the output - // TrackEvent.trace("beacon", "Sending raw client error:\n" + json.toPrettyString()); - - var mapper = JacksonMapper.getDefault(); - try (OutputStream blockOut = BeaconFormat.writeBlocks(outSocket.getOutputStream())) { - var gen = mapper.createGenerator(blockOut); - gen.writeTree(msg); - } - } - - public void sendServerErrorResponse(Socket outSocket, Throwable ex) throws Exception { - var err = new ServerErrorMessage(UUID.randomUUID(), ex); - ObjectNode json = JacksonMapper.getDefault().valueToTree(err); - var msg = JsonNodeFactory.instance.objectNode(); - msg.set("xPipeServerError", json); - - // Don't log this as it clutters the output - // TrackEvent.trace("beacon", "Sending raw server error:\n" + json.toPrettyString()); - - var mapper = JacksonMapper.getDefault(); - try (OutputStream blockOut = BeaconFormat.writeBlocks(outSocket.getOutputStream())) { - var gen = mapper.createGenerator(blockOut); - gen.writeTree(msg); - } - } - - private T parseRequest(JsonNode header) throws Exception { - ObjectNode content = (ObjectNode) header.required("xPipeMessage"); - TrackEvent.trace("Parsed raw request:\n" + content.toPrettyString()); - - var type = content.required("messageType").textValue(); - var phase = content.required("messagePhase").textValue(); - if (!phase.equals("request")) { - throw new IllegalArgumentException("Not a request"); - } - content.remove("messageType"); - content.remove("messagePhase"); - - var prov = MessageExchangeImpls.byId(type); - if (prov.isEmpty()) { - throw new IllegalArgumentException("Unknown request id: " + type); - } - - var reader = JacksonMapper.getDefault().readerFor(prov.get().getRequestClass()); - return reader.readValue(content); - } -} 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 c126f0e0..380a4f05 100644 --- a/app/src/main/java/io/xpipe/app/core/AppTheme.java +++ b/app/src/main/java/io/xpipe/app/core/AppTheme.java @@ -235,10 +235,7 @@ public class AppTheme { .collect(Collectors.joining("\n"))); }); - var out = Files.createTempFile(id, ".css"); - Files.writeString(out, builder.toString()); - - Application.setUserAgentStylesheet(out.toUri().toString()); + Application.setUserAgentStylesheet(Styles.toDataURI(builder.toString())); } @Override @@ -257,13 +254,14 @@ public class AppTheme { public static final Theme CUPERTINO_LIGHT = new Theme("cupertinoLight", "cupertino", new CupertinoLight()); public static final Theme CUPERTINO_DARK = new Theme("cupertinoDark", "cupertino", new CupertinoDark()); public static final Theme DRACULA = new Theme("dracula", "dracula", new Dracula()); + public static final Theme MOCHA = new DerivedTheme("mocha", "primer", "Mocha", new PrimerDark()); // Adjust this to create your own theme public static final Theme CUSTOM = new DerivedTheme("custom", "primer", "Custom", new PrimerDark()); // Also include your custom theme here public static final List ALL = - List.of(PRIMER_LIGHT, PRIMER_DARK, NORD_LIGHT, NORD_DARK, CUPERTINO_LIGHT, CUPERTINO_DARK, DRACULA); + List.of(PRIMER_LIGHT, PRIMER_DARK, NORD_LIGHT, NORD_DARK, CUPERTINO_LIGHT, CUPERTINO_DARK, DRACULA, MOCHA); protected final String id; @Getter 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 d3b8582d..63f840d9 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 @@ -1,5 +1,6 @@ package io.xpipe.app.core.mode; +import io.xpipe.app.beacon.AppBeaconServer; import io.xpipe.app.browser.session.BrowserSessionModel; import io.xpipe.app.comp.store.StoreViewState; import io.xpipe.app.core.*; @@ -17,7 +18,6 @@ import io.xpipe.app.util.FileBridge; import io.xpipe.app.util.LicenseProvider; import io.xpipe.app.util.LocalShell; import io.xpipe.app.util.UnlockAlert; -import io.xpipe.core.util.JacksonMapper; public class BaseMode extends OperationMode { @@ -43,12 +43,8 @@ public class BaseMode extends OperationMode { // if (true) throw new IllegalStateException(); TrackEvent.info("Initializing base mode components ..."); - AppExtensionManager.init(true); - JacksonMapper.initModularized(AppExtensionManager.getInstance().getExtendedLayer()); AppI18n.init(); LicenseProvider.get().init(); - AppPrefs.initLocal(); - AppI18n.init(); AppCertutilCheck.check(); AppAvCheck.check(); AppSid.init(); @@ -56,8 +52,8 @@ public class BaseMode extends OperationMode { AppShellCheck.check(); XPipeDistributionType.init(); AppPrefs.setDefaults(); - // Initialize socket server as we should be prepared for git askpass commands - AppSocketServer.init(); + // Initialize beacon server as we should be prepared for git askpass commands + AppBeaconServer.init(); GitStorageHandler.getInstance().init(); GitStorageHandler.getInstance().setupRepositoryAndPull(); AppPrefs.initSharedRemote(); @@ -85,8 +81,8 @@ public class BaseMode extends OperationMode { AppResources.reset(); AppExtensionManager.reset(); AppDataLock.unlock(); - // Shut down socket server last to keep a non-daemon thread running - AppSocketServer.reset(); + // Shut down server last to keep a non-daemon thread running + AppBeaconServer.reset(); TrackEvent.info("Background mode shutdown finished"); } } 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 ad3633e6..44edc606 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 @@ -6,6 +6,7 @@ import io.xpipe.app.core.check.AppTempCheck; import io.xpipe.app.core.check.AppUserDirectoryCheck; import io.xpipe.app.issue.*; import io.xpipe.app.launcher.LauncherCommand; +import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.LocalShell; import io.xpipe.app.util.PlatformState; import io.xpipe.app.util.ThreadHelper; @@ -109,6 +110,9 @@ public abstract class OperationMode { AppProperties.logArguments(args); AppProperties.logSystemProperties(); AppProperties.logPassedProperties(); + AppExtensionManager.init(true); + AppI18n.init(); + AppPrefs.initLocal(); TrackEvent.info("Finished initial setup"); } catch (Throwable ex) { ErrorEvent.fromThrowable(ex).term().handle(); diff --git a/app/src/main/java/io/xpipe/app/exchange/AskpassExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/AskpassExchangeImpl.java deleted file mode 100644 index 810d2c71..00000000 --- a/app/src/main/java/io/xpipe/app/exchange/AskpassExchangeImpl.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.xpipe.app.exchange; - -import io.xpipe.app.util.SecretManager; -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.exchange.AskpassExchange; - -public class AskpassExchangeImpl extends AskpassExchange - implements MessageExchangeImpl { - - @Override - public Response handleRequest(BeaconHandler handler, Request msg) { - var found = msg.getSecretId() != null - ? SecretManager.getProgress(msg.getRequest(), msg.getSecretId()) - : SecretManager.getProgress(msg.getRequest()); - if (found.isEmpty()) { - return Response.builder().build(); - } - - var p = found.get(); - var secret = p.process(msg.getPrompt()); - return Response.builder() - .value(secret != null ? secret.inPlace() : null) - .build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/exchange/DialogExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/DialogExchangeImpl.java deleted file mode 100644 index b6d46c20..00000000 --- a/app/src/main/java/io/xpipe/app/exchange/DialogExchangeImpl.java +++ /dev/null @@ -1,76 +0,0 @@ -package io.xpipe.app.exchange; - -import io.xpipe.app.issue.TrackEvent; -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.exchange.cli.DialogExchange; -import io.xpipe.core.dialog.Dialog; -import io.xpipe.core.dialog.DialogReference; -import io.xpipe.core.util.FailableConsumer; - -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -public class DialogExchangeImpl extends DialogExchange - implements MessageExchangeImpl { - - private static final Map openDialogs = new HashMap<>(); - private static final Map> openDialogConsumers = new HashMap<>(); - - public static DialogReference add(Dialog d, FailableConsumer onCompletion) throws Exception { - return add(d, UUID.randomUUID(), onCompletion); - } - - public static DialogReference add(Dialog d, UUID uuid, FailableConsumer onCompletion) - throws Exception { - openDialogs.put(uuid, d); - openDialogConsumers.put(uuid, onCompletion); - return new DialogReference(uuid, d.start()); - } - - @Override - public DialogExchange.Response handleRequest(BeaconHandler handler, Request msg) throws Exception { - if (msg.isCancel()) { - TrackEvent.withTrace("Received cancel dialog request") - .tag("key", msg.getDialogKey()) - .handle(); - openDialogs.remove(msg.getDialogKey()); - openDialogConsumers.remove(msg.getDialogKey()); - return DialogExchange.Response.builder().element(null).build(); - } - - var dialog = openDialogs.get(msg.getDialogKey()); - var e = dialog.receive(msg.getValue()); - - TrackEvent.withTrace("Received normal dialog request") - .tag("key", msg.getDialogKey()) - .tag("value", msg.getValue()) - .tag("newElement", e) - .handle(); - - if (e == null) { - openDialogs.remove(msg.getDialogKey()); - var con = openDialogConsumers.remove(msg.getDialogKey()); - con.accept(dialog.getResult()); - } - - return DialogExchange.Response.builder().element(e).build(); - // - // - // var provider = getProvider(msg.getInstance().getProvider()); - // var completeConfig = toCompleteConfig(provider); - // - // var option = completeConfig.keySet().stream() - // .filter(o -> o.getKey().equals(msg.getKey())).findAny() - // .orElseThrow(() -> new ClientException("Invalid config key: " + msg.getKey())); - // - // String errorMsg = null; - // try { - // option.getConverter().convertFromString(msg.getValue()); - // } catch (Exception ex) { - // errorMsg = ex.getMessage(); - // } - // - // return DialogExchange.Response.builder().errorMsg(errorMsg).build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/exchange/FocusExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/FocusExchangeImpl.java deleted file mode 100644 index 2b5181d5..00000000 --- a/app/src/main/java/io/xpipe/app/exchange/FocusExchangeImpl.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.xpipe.app.exchange; - -import io.xpipe.app.core.mode.OperationMode; -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.exchange.FocusExchange; - -public class FocusExchangeImpl extends FocusExchange - implements MessageExchangeImpl { - - @Override - public Response handleRequest(BeaconHandler handler, Request msg) { - OperationMode.switchUp(OperationMode.map(msg.getMode())); - return Response.builder().build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/exchange/LaunchExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/LaunchExchangeImpl.java deleted file mode 100644 index a7dfb5a7..00000000 --- a/app/src/main/java/io/xpipe/app/exchange/LaunchExchangeImpl.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.xpipe.app.exchange; - -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.exchange.LaunchExchange; -import io.xpipe.core.store.LaunchableStore; - -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -public class LaunchExchangeImpl extends LaunchExchange - implements MessageExchangeImpl { - - @Override - public Response handleRequest(BeaconHandler handler, Request msg) throws Exception { - var store = getStoreEntryById(msg.getId(), false); - if (store.getStore() instanceof LaunchableStore s) { - // var command = s.prepareLaunchCommand() - // .prepareTerminalOpen(TerminalInitScriptConfig.ofName(store.getName()), sc -> null); - // return Response.builder().command(split(command)).build(); - } - - throw new IllegalArgumentException(store.getName() + " is not launchable"); - } - - private List split(String command) { - var split = Arrays.stream(command.split(" ", 3)).collect(Collectors.toList()); - var s = split.get(2); - if ((s.startsWith("\"") && s.endsWith("\"")) || (s.startsWith("'") && s.endsWith("'"))) { - split.set(2, s.substring(1, s.length() - 1)); - } - return split; - } -} diff --git a/app/src/main/java/io/xpipe/app/exchange/MessageExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/MessageExchangeImpl.java deleted file mode 100644 index f73b7583..00000000 --- a/app/src/main/java/io/xpipe/app/exchange/MessageExchangeImpl.java +++ /dev/null @@ -1,47 +0,0 @@ -package io.xpipe.app.exchange; - -import io.xpipe.app.storage.DataStorage; -import io.xpipe.app.storage.DataStoreEntry; -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.ClientException; -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.beacon.exchange.MessageExchange; -import io.xpipe.core.store.DataStoreId; - -import lombok.NonNull; - -public interface MessageExchangeImpl extends MessageExchange { - - default DataStoreEntry getStoreEntryByName(@NonNull String name, boolean acceptDisabled) throws ClientException { - var store = DataStorage.get().getStoreEntryIfPresent(name); - if (store.isEmpty()) { - throw new ClientException("No store with name " + name + " was found"); - } - if (store.get().isDisabled() && !acceptDisabled) { - throw new ClientException( - String.format("Store %s is disabled", store.get().getName())); - } - return store.get(); - } - - default DataStoreEntry getStoreEntryById(@NonNull DataStoreId id, boolean acceptUnusable) throws ClientException { - var store = DataStorage.get().getStoreEntryIfPresent(id); - if (store.isEmpty()) { - throw new ClientException("No store with id " + id + " was found"); - } - if (store.get().isDisabled() && !acceptUnusable) { - throw new ClientException( - String.format("Store %s is disabled", store.get().getName())); - } - if (!store.get().getValidity().isUsable() && !acceptUnusable) { - throw new ClientException(String.format( - "Store %s is not completely configured", store.get().getName())); - } - return store.get(); - } - - String getId(); - - RS handleRequest(BeaconHandler handler, RQ msg) throws Exception; -} diff --git a/app/src/main/java/io/xpipe/app/exchange/MessageExchangeImpls.java b/app/src/main/java/io/xpipe/app/exchange/MessageExchangeImpls.java deleted file mode 100644 index 94a18b15..00000000 --- a/app/src/main/java/io/xpipe/app/exchange/MessageExchangeImpls.java +++ /dev/null @@ -1,61 +0,0 @@ -package io.xpipe.app.exchange; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.beacon.exchange.MessageExchanges; -import io.xpipe.core.util.ModuleLayerLoader; - -import java.util.List; -import java.util.Optional; -import java.util.ServiceLoader; -import java.util.stream.Collectors; - -public class MessageExchangeImpls { - - private static List> ALL; - - @SuppressWarnings("unchecked") - public static Optional> byId( - String name) { - var r = ALL.stream().filter(d -> d.getId().equals(name)).findAny(); - return Optional.ofNullable((MessageExchangeImpl) r.orElse(null)); - } - - @SuppressWarnings("unchecked") - public static - Optional> byRequest(RQ req) { - var r = ALL.stream() - .filter(d -> d.getRequestClass().equals(req.getClass())) - .findAny(); - return Optional.ofNullable((MessageExchangeImpl) r.orElse(null)); - } - - public static List> getAll() { - return ALL; - } - - public static class Loader implements ModuleLayerLoader { - - @Override - public void init(ModuleLayer layer) { - ALL = ServiceLoader.load(layer, MessageExchangeImpl.class).stream() - .map(s -> { - // TrackEvent.trace("init", "Loaded exchange implementation " + ex.getId()); - return (MessageExchangeImpl) s.get(); - }) - .collect(Collectors.toList()); - - ALL.forEach(messageExchange -> { - if (MessageExchanges.byId(messageExchange.getId()).isEmpty()) { - throw new AssertionError("Missing base exchange: " + messageExchange.getId()); - } - }); - - MessageExchanges.getAll().forEach(messageExchange -> { - if (MessageExchangeImpls.byId(messageExchange.getId()).isEmpty()) { - throw new AssertionError("Missing exchange implementation: " + messageExchange.getId()); - } - }); - } - } -} diff --git a/app/src/main/java/io/xpipe/app/exchange/OpenExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/OpenExchangeImpl.java deleted file mode 100644 index 9a560177..00000000 --- a/app/src/main/java/io/xpipe/app/exchange/OpenExchangeImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.xpipe.app.exchange; - -import io.xpipe.app.core.mode.OperationMode; -import io.xpipe.app.launcher.LauncherInput; -import io.xpipe.app.util.PlatformState; -import io.xpipe.app.util.ThreadHelper; -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.ServerException; -import io.xpipe.beacon.exchange.OpenExchange; - -public class OpenExchangeImpl extends OpenExchange - implements MessageExchangeImpl { - - @Override - public Response handleRequest(BeaconHandler handler, Request msg) throws ServerException { - if (msg.getArguments().isEmpty()) { - if (!OperationMode.switchToSyncIfPossible(OperationMode.GUI)) { - throw new ServerException(PlatformState.getLastError()); - } - } - - // Wait for startup - while (OperationMode.get() == null) { - ThreadHelper.sleep(100); - } - - LauncherInput.handle(msg.getArguments()); - return Response.builder().build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/exchange/QueryStoreExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/QueryStoreExchangeImpl.java deleted file mode 100644 index 39f54659..00000000 --- a/app/src/main/java/io/xpipe/app/exchange/QueryStoreExchangeImpl.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.xpipe.app.exchange; - -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.exchange.QueryStoreExchange; -import io.xpipe.core.dialog.DialogMapper; - -public class QueryStoreExchangeImpl extends QueryStoreExchange - implements MessageExchangeImpl { - - @Override - public Response handleRequest(BeaconHandler handler, Request msg) throws Exception { - var store = getStoreEntryByName(msg.getName(), true); - var summary = ""; - var dialog = store.getProvider().dialogForStore(store.getStore().asNeeded()); - var config = new DialogMapper(dialog).handle(); - return Response.builder() - .summary(summary) - .internalStore(store.getStore()) - .provider(store.getProvider().getId()) - .config(config) - .build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/exchange/TerminalLaunchExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/TerminalLaunchExchangeImpl.java deleted file mode 100644 index 5540de7f..00000000 --- a/app/src/main/java/io/xpipe/app/exchange/TerminalLaunchExchangeImpl.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.xpipe.app.exchange; - -import io.xpipe.app.util.TerminalLauncherManager; -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.ClientException; -import io.xpipe.beacon.exchange.TerminalLaunchExchange; - -public class TerminalLaunchExchangeImpl extends TerminalLaunchExchange - implements MessageExchangeImpl { - - @Override - public Response handleRequest(BeaconHandler handler, Request msg) throws ClientException { - var r = TerminalLauncherManager.performLaunch(msg.getRequest()); - return Response.builder().targetFile(r).build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/exchange/TerminalWaitExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/TerminalWaitExchangeImpl.java deleted file mode 100644 index bac98a9e..00000000 --- a/app/src/main/java/io/xpipe/app/exchange/TerminalWaitExchangeImpl.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.xpipe.app.exchange; - -import io.xpipe.app.util.TerminalLauncherManager; -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.ClientException; -import io.xpipe.beacon.ServerException; -import io.xpipe.beacon.exchange.TerminalWaitExchange; - -public class TerminalWaitExchangeImpl extends TerminalWaitExchange - implements MessageExchangeImpl { - - @Override - public Response handleRequest(BeaconHandler handler, Request msg) throws ServerException, ClientException { - TerminalLauncherManager.waitForCompletion(msg.getRequest()); - return Response.builder().build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/exchange/cli/DrainExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/cli/DrainExchangeImpl.java deleted file mode 100644 index 5cdd9b15..00000000 --- a/app/src/main/java/io/xpipe/app/exchange/cli/DrainExchangeImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.xpipe.app.exchange.cli; - -import io.xpipe.app.exchange.MessageExchangeImpl; -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.ClientException; -import io.xpipe.beacon.exchange.DrainExchange; -import io.xpipe.core.store.ShellStore; - -public class DrainExchangeImpl extends DrainExchange - implements MessageExchangeImpl { - - @Override - public Response handleRequest(BeaconHandler handler, Request msg) throws Exception { - var ds = getStoreEntryById(msg.getSource(), false); - - if (!(ds.getStore() instanceof ShellStore)) { - throw new ClientException("Can't open file system for connection"); - } - - handler.postResponse(() -> { - ShellStore store = ds.getStore().asNeeded(); - try (var fs = store.createFileSystem(); - var output = handler.sendBody(); - var inputStream = fs.openInput(msg.getPath())) { - inputStream.transferTo(output); - } - }); - return Response.builder().build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/exchange/cli/EditStoreExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/cli/EditStoreExchangeImpl.java deleted file mode 100644 index b13c3045..00000000 --- a/app/src/main/java/io/xpipe/app/exchange/cli/EditStoreExchangeImpl.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.xpipe.app.exchange.cli; - -import io.xpipe.app.exchange.DialogExchangeImpl; -import io.xpipe.app.exchange.MessageExchangeImpl; -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.exchange.cli.EditStoreExchange; -import io.xpipe.core.store.DataStore; - -public class EditStoreExchangeImpl extends EditStoreExchange - implements MessageExchangeImpl { - - @Override - public Response handleRequest(BeaconHandler handler, Request msg) throws Exception { - var s = getStoreEntryByName(msg.getName(), false); - var dialog = s.getProvider().dialogForStore(s.getStore()); - var reference = DialogExchangeImpl.add(dialog, (DataStore newStore) -> { - // s.setStore(newStore); - }); - return Response.builder().dialog(reference).build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/exchange/cli/ListStoresExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/cli/ListStoresExchangeImpl.java deleted file mode 100644 index 09a44977..00000000 --- a/app/src/main/java/io/xpipe/app/exchange/cli/ListStoresExchangeImpl.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.xpipe.app.exchange.cli; - -import io.xpipe.app.exchange.MessageExchangeImpl; -import io.xpipe.app.storage.DataStorage; -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.exchange.cli.ListStoresExchange; -import io.xpipe.beacon.exchange.data.StoreListEntry; - -import java.util.Comparator; -import java.util.List; - -public class ListStoresExchangeImpl extends ListStoresExchange - implements MessageExchangeImpl { - - @Override - public Response handleRequest(BeaconHandler handler, Request msg) { - DataStorage s = DataStorage.get(); - if (s == null) { - return Response.builder().entries(List.of()).build(); - } - - var e = s.getStoreEntries().stream() - .filter(entry -> !entry.isDisabled()) - .map(col -> StoreListEntry.builder() - .id(DataStorage.get().getId(col)) - .type(col.getProvider().getId()) - .build()) - .sorted(Comparator.comparing(en -> en.getId().toString())) - .toList(); - return Response.builder().entries(e).build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/exchange/cli/ModeExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/cli/ModeExchangeImpl.java deleted file mode 100644 index d08a88c6..00000000 --- a/app/src/main/java/io/xpipe/app/exchange/cli/ModeExchangeImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.xpipe.app.exchange.cli; - -import io.xpipe.app.core.mode.OperationMode; -import io.xpipe.app.exchange.MessageExchangeImpl; -import io.xpipe.app.util.ThreadHelper; -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.ClientException; -import io.xpipe.beacon.exchange.cli.ModeExchange; - -public class ModeExchangeImpl extends ModeExchange - implements MessageExchangeImpl { - - @Override - public Response handleRequest(BeaconHandler handler, Request msg) throws Exception { - // Wait for startup - while (OperationMode.get() == null) { - ThreadHelper.sleep(100); - } - - var mode = OperationMode.map(msg.getMode()); - if (!mode.isSupported()) { - throw new ClientException("Unsupported mode: " + msg.getMode().getDisplayName() + ". Supported: " - + String.join( - ", ", - OperationMode.getAll().stream() - .filter(OperationMode::isSupported) - .map(OperationMode::getId) - .toList())); - } - - OperationMode.switchToSyncIfPossible(mode); - return ModeExchange.Response.builder() - .usedMode(OperationMode.map(OperationMode.get())) - .build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/exchange/cli/ReadDrainExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/cli/ReadDrainExchangeImpl.java deleted file mode 100644 index 2cc03a8f..00000000 --- a/app/src/main/java/io/xpipe/app/exchange/cli/ReadDrainExchangeImpl.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.xpipe.app.exchange.cli; - -import io.xpipe.app.exchange.MessageExchangeImpl; -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.exchange.cli.ReadDrainExchange; - -public class ReadDrainExchangeImpl extends ReadDrainExchange - implements MessageExchangeImpl { - - @Override - public Response handleRequest(BeaconHandler handler, Request msg) { - return ReadDrainExchange.Response.builder().build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/exchange/cli/RemoveStoreExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/cli/RemoveStoreExchangeImpl.java deleted file mode 100644 index 25ee3208..00000000 --- a/app/src/main/java/io/xpipe/app/exchange/cli/RemoveStoreExchangeImpl.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.xpipe.app.exchange.cli; - -import io.xpipe.app.exchange.MessageExchangeImpl; -import io.xpipe.app.storage.DataStorage; -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.ClientException; -import io.xpipe.beacon.exchange.cli.RemoveStoreExchange; -import io.xpipe.core.store.DataStoreId; - -public class RemoveStoreExchangeImpl extends RemoveStoreExchange - implements MessageExchangeImpl { - - @Override - public Response handleRequest(BeaconHandler handler, Request msg) throws Exception { - var s = getStoreEntryById(DataStoreId.fromString(msg.getStoreName()), true); - if (!s.getConfiguration().isDeletable()) { - throw new ClientException("Store is not deletable"); - } - - DataStorage.get().deleteStoreEntry(s); - return Response.builder().build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/exchange/cli/RenameStoreExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/cli/RenameStoreExchangeImpl.java deleted file mode 100644 index eccd5546..00000000 --- a/app/src/main/java/io/xpipe/app/exchange/cli/RenameStoreExchangeImpl.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.xpipe.app.exchange.cli; - -import io.xpipe.app.exchange.MessageExchangeImpl; -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.ClientException; -import io.xpipe.beacon.exchange.cli.RenameStoreExchange; -import io.xpipe.core.store.DataStoreId; - -public class RenameStoreExchangeImpl extends RenameStoreExchange - implements MessageExchangeImpl { - - @Override - public Response handleRequest(BeaconHandler handler, Request msg) throws ClientException { - var s = getStoreEntryById(DataStoreId.fromString(msg.getStoreName()), true); - s.setName(msg.getNewName()); - return Response.builder().build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/exchange/cli/SinkExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/cli/SinkExchangeImpl.java deleted file mode 100644 index 79ddef7f..00000000 --- a/app/src/main/java/io/xpipe/app/exchange/cli/SinkExchangeImpl.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.xpipe.app.exchange.cli; - -import io.xpipe.app.exchange.MessageExchangeImpl; -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.ClientException; -import io.xpipe.beacon.exchange.SinkExchange; -import io.xpipe.core.store.ShellStore; - -public class SinkExchangeImpl extends SinkExchange - implements MessageExchangeImpl { - - @Override - public Response handleRequest(BeaconHandler handler, Request msg) throws Exception { - var ds = getStoreEntryById(msg.getSource(), false); - - if (!(ds.getStore() instanceof ShellStore)) { - throw new ClientException("Can't open file system for connection"); - } - - ShellStore store = ds.getStore().asNeeded(); - try (var fs = store.createFileSystem(); - var inputStream = handler.receiveBody(); - var output = fs.openOutput(msg.getPath(), -1)) { - inputStream.transferTo(output); - } - - return Response.builder().build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/exchange/cli/StatusExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/cli/StatusExchangeImpl.java deleted file mode 100644 index 520a2e91..00000000 --- a/app/src/main/java/io/xpipe/app/exchange/cli/StatusExchangeImpl.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.xpipe.app.exchange.cli; - -import io.xpipe.app.core.mode.OperationMode; -import io.xpipe.app.exchange.MessageExchangeImpl; -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.exchange.cli.StatusExchange; - -public class StatusExchangeImpl extends StatusExchange - implements MessageExchangeImpl { - - @Override - public Response handleRequest(BeaconHandler handler, Request msg) { - String mode; - if (OperationMode.get() == null) { - mode = "none"; - } else { - mode = OperationMode.get().getId(); - } - - return Response.builder().mode(mode).build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/exchange/cli/StopExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/cli/StopExchangeImpl.java deleted file mode 100644 index df02c0b6..00000000 --- a/app/src/main/java/io/xpipe/app/exchange/cli/StopExchangeImpl.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.xpipe.app.exchange.cli; - -import io.xpipe.app.core.mode.OperationMode; -import io.xpipe.app.exchange.MessageExchangeImpl; -import io.xpipe.app.util.ThreadHelper; -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.exchange.StopExchange; - -public class StopExchangeImpl extends StopExchange - implements MessageExchangeImpl { - - @Override - public Response handleRequest(BeaconHandler handler, Request msg) { - handler.postResponse(() -> { - ThreadHelper.runAsync(() -> { - ThreadHelper.sleep(1000); - OperationMode.close(); - }); - }); - return Response.builder().success(true).build(); - } -} diff --git a/app/src/main/java/io/xpipe/app/exchange/cli/StoreAddExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/cli/StoreAddExchangeImpl.java deleted file mode 100644 index ee47a65a..00000000 --- a/app/src/main/java/io/xpipe/app/exchange/cli/StoreAddExchangeImpl.java +++ /dev/null @@ -1,141 +0,0 @@ -package io.xpipe.app.exchange.cli; - -import io.xpipe.app.exchange.DialogExchangeImpl; -import io.xpipe.app.exchange.MessageExchangeImpl; -import io.xpipe.app.ext.DataStoreProvider; -import io.xpipe.app.ext.DataStoreProviders; -import io.xpipe.app.storage.DataStorage; -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.ClientException; -import io.xpipe.beacon.exchange.cli.StoreAddExchange; -import io.xpipe.core.dialog.Choice; -import io.xpipe.core.dialog.Dialog; -import io.xpipe.core.dialog.QueryConverter; -import io.xpipe.core.store.DataStore; - -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; - -import lombok.SneakyThrows; - -import java.util.List; - -public class StoreAddExchangeImpl extends StoreAddExchange - implements MessageExchangeImpl { - - @Override - @SneakyThrows - public StoreAddExchange.Response handleRequest(BeaconHandler handler, Request msg) { - Dialog creatorDialog; - DataStoreProvider provider; - if (msg.getStoreInput() != null) { - creatorDialog = Dialog.empty().evaluateTo(msg::getStoreInput); - provider = null; - } else { - if (msg.getType() == null) { - throw new ClientException("Missing data store tight"); - } - - provider = DataStoreProviders.byName(msg.getType()).orElseThrow(() -> { - return new ClientException("Unrecognized data store type: " + msg.getType()); - }); - - creatorDialog = provider.dialogForStore(provider.defaultStore()); - } - - var name = new SimpleStringProperty(msg.getName()); - var completeDialog = createCompleteDialog(provider, creatorDialog, name); - var config = DialogExchangeImpl.add(completeDialog, (DataStore store) -> { - if (store == null) { - return; - } - - DataStorage.get().addStoreIfNotPresent(name.getValue(), store); - }); - - return StoreAddExchange.Response.builder().config(config).build(); - } - - private Dialog createCompleteDialog(DataStoreProvider provider, Dialog creator, StringProperty name) { - var validator = Dialog.header(() -> { - DataStore store = creator.getResult(); - if (store == null) { - return "Store is null"; - } - - return null; - }) - .map((String msg) -> { - return msg == null ? creator.getResult() : null; - }); - - var creatorAndValidator = Dialog.chain(creator, Dialog.busy(), validator); - - var nameQ = Dialog.retryIf( - Dialog.query("Store name", true, true, false, name.getValue(), QueryConverter.STRING), - (String r) -> { - return DataStorage.get().getStoreEntryIfPresent(r).isPresent() - ? "Store with name " + r + " already exists" - : null; - }) - .onCompletion((String n) -> name.setValue(n)); - - var display = Dialog.header(() -> { - if (provider == null) { - return "Successfully created data store " + name.get(); - } - - DataStore s = creator.getResult(); - String d = ""; - d = d.indent(2); - return "Successfully created data store " + name.get() + ":\n" + d; - }); - - if (provider == null) { - return Dialog.chain( - creatorAndValidator, Dialog.skipIf(display, () -> creatorAndValidator.getResult() == null)) - .evaluateTo(creatorAndValidator); - } - - var aborted = new SimpleBooleanProperty(); - var addStore = - Dialog.skipIf(Dialog.chain(nameQ, display), () -> aborted.get() || validator.getResult() == null); - - var prop = new SimpleObjectProperty(); - var fork = Dialog.skipIf( - Dialog.fork( - "Choose how to continue", - List.of( - new Choice('r', "Retry"), - new Choice('i', "Ignore and continue"), - new Choice('e', "Edit configuration"), - new Choice('a', "Abort")), - true, - 0, - (Integer choice) -> { - if (choice == 0) { - return Dialog.chain(Dialog.busy(), validator, prop.get()); - } - if (choice == 1) { - return null; - } - if (choice == 2) { - return Dialog.chain(creatorAndValidator, prop.get()); - } - if (choice == 3) { - aborted.set(true); - return null; - } - - throw new AssertionError(); - }) - .evaluateTo(() -> null), - () -> validator.getResult() != null); - prop.set(fork); - - return Dialog.chain(creatorAndValidator, fork, addStore) - .evaluateTo(() -> aborted.get() ? null : creator.getResult()); - } -} diff --git a/app/src/main/java/io/xpipe/app/exchange/cli/StoreProviderListExchangeImpl.java b/app/src/main/java/io/xpipe/app/exchange/cli/StoreProviderListExchangeImpl.java deleted file mode 100644 index b8986104..00000000 --- a/app/src/main/java/io/xpipe/app/exchange/cli/StoreProviderListExchangeImpl.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.xpipe.app.exchange.cli; - -import io.xpipe.app.exchange.MessageExchangeImpl; -import io.xpipe.app.ext.DataStoreProvider; -import io.xpipe.app.ext.DataStoreProviders; -import io.xpipe.beacon.BeaconHandler; -import io.xpipe.beacon.exchange.cli.StoreProviderListExchange; -import io.xpipe.beacon.exchange.data.ProviderEntry; - -import java.util.Arrays; -import java.util.stream.Collectors; - -public class StoreProviderListExchangeImpl extends StoreProviderListExchange - implements MessageExchangeImpl { - - @Override - public Response handleRequest(BeaconHandler handler, Request msg) { - var categories = DataStoreProvider.CreationCategory.values(); - var all = DataStoreProviders.getAll(); - var map = Arrays.stream(categories) - .collect(Collectors.toMap(category -> getName(category), category -> all.stream() - .filter(dataStoreProvider -> category.equals(dataStoreProvider.getCreationCategory())) - .map(p -> ProviderEntry.builder() - .id(p.getId()) - .description(p.displayDescription().getValue()) - .hidden(p.getCreationCategory() == null) - .build()) - .toList())); - - return Response.builder().entries(map).build(); - } - - private String getName(DataStoreProvider.CreationCategory category) { - return category.name().substring(0, 1).toUpperCase() - + category.name().substring(1).toLowerCase(); - } -} 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 2d1ddad4..76d8f67b 100644 --- a/app/src/main/java/io/xpipe/app/ext/ActionProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/ActionProvider.java @@ -33,10 +33,6 @@ public interface ActionProvider { return null; } - default boolean isActive() { - return true; - } - default String getProFeatureId() { return null; } @@ -45,7 +41,12 @@ public interface ActionProvider { return null; } - default DataStoreCallSite getDataStoreCallSite() { + default LeafDataStoreCallSite getLeafDataStoreCallSite() { + return null; + } + + + default BranchDataStoreCallSite getBranchDataStoreCallSite() { return null; } @@ -55,8 +56,6 @@ public interface ActionProvider { interface Action { - boolean requiresJavaFXPlatform(); - void execute() throws Exception; } @@ -89,6 +88,10 @@ public interface ActionProvider { default boolean isApplicable(DataStoreEntryRef o) { return true; } + + default boolean showBusy() { + return true; + } } interface DataStoreCallSite { @@ -97,14 +100,6 @@ public interface ActionProvider { return false; } - default boolean canLinkTo() { - return false; - } - - Action createAction(DataStoreEntryRef store); - - Class getApplicableClass(); - default boolean isMajor(DataStoreEntryRef o) { return false; } @@ -117,14 +112,30 @@ public interface ActionProvider { String getIcon(DataStoreEntryRef store); - default ActiveType activeType() { - return ActiveType.ONLY_SHOW_IF_ENABLED; + Class getApplicableClass(); + + default boolean showBusy() { + return true; + } + } + + interface BranchDataStoreCallSite extends DataStoreCallSite { + + default List getChildren() { + return List.of(); + } + } + + interface LeafDataStoreCallSite extends DataStoreCallSite { + + default boolean canLinkTo() { + return false; } - enum ActiveType { - ONLY_SHOW_IF_ENABLED, - ALWAYS_SHOW, - ALWAYS_ENABLE + Action createAction(DataStoreEntryRef store); + + default boolean requiresValidStore() { + return true; } } @@ -134,14 +145,6 @@ public interface ActionProvider { public void init(ModuleLayer layer) { ALL.addAll(ServiceLoader.load(layer, ActionProvider.class).stream() .map(actionProviderProvider -> actionProviderProvider.get()) - .filter(provider -> { - try { - return provider.isActive(); - } catch (Throwable e) { - ErrorEvent.fromThrowable(e).handle(); - return false; - } - }) .toList()); } } diff --git a/app/src/main/java/io/xpipe/app/ext/DataStorageExtensionProvider.java b/app/src/main/java/io/xpipe/app/ext/DataStorageExtensionProvider.java new file mode 100644 index 00000000..94672676 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/ext/DataStorageExtensionProvider.java @@ -0,0 +1,31 @@ +package io.xpipe.app.ext; + +import io.xpipe.core.util.ModuleLayerLoader; + +import java.util.Comparator; +import java.util.List; +import java.util.ServiceLoader; +import java.util.stream.Collectors; + +public abstract class DataStorageExtensionProvider { + + private static List ALL; + + public static List getAll() { + return ALL; + } + + public void storageInit() throws Exception {} + + public static class Loader implements ModuleLayerLoader { + + @Override + public void init(ModuleLayer layer) { + ALL = ServiceLoader.load(layer, DataStorageExtensionProvider.class).stream() + .map(ServiceLoader.Provider::get) + .sorted(Comparator.comparing( + scanProvider -> scanProvider.getClass().getName())) + .collect(Collectors.toList()); + } + } +} 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 b05302b0..b3cbaf3e 100644 --- a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java @@ -12,10 +12,8 @@ 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.dialog.Dialog; import io.xpipe.core.store.DataStore; import io.xpipe.core.util.JacksonizedValue; - import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.Property; @@ -31,7 +29,9 @@ public interface DataStoreProvider { default boolean shouldShow(StoreEntryWrapper w) { return true; } - + + default void onChildrenRefresh(DataStoreEntry entry) {} + default ObservableBooleanValue busy(StoreEntryWrapper wrapper) { return new SimpleBooleanProperty(false); } @@ -196,10 +196,6 @@ public interface DataStoreProvider { return getModuleName() + ":" + getId() + "_icon.svg"; } - default Dialog dialogForStore(DataStore store) { - return null; - } - default DataStore defaultStore() { return null; } @@ -216,6 +212,7 @@ public interface DataStoreProvider { HOST, DATABASE, SHELL, + SERVICE, COMMAND, TUNNEL, SCRIPT, diff --git a/app/src/main/java/io/xpipe/app/ext/EnabledParentStoreProvider.java b/app/src/main/java/io/xpipe/app/ext/EnabledParentStoreProvider.java new file mode 100644 index 00000000..24935e30 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/ext/EnabledParentStoreProvider.java @@ -0,0 +1,40 @@ +package io.xpipe.app.ext; + +import io.xpipe.app.comp.base.StoreToggleComp; +import io.xpipe.app.comp.store.StoreEntryComp; +import io.xpipe.app.comp.store.StoreSection; +import io.xpipe.app.comp.store.StoreViewState; +import io.xpipe.app.fxcomps.util.BindingsHelper; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.core.store.EnabledStoreState; +import io.xpipe.core.store.StatefulDataStore; + +public interface EnabledParentStoreProvider extends DataStoreProvider { + + @Override + public default StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) { + if (sec.getWrapper().getValidity().getValue() != DataStoreEntry.Validity.COMPLETE) { + return StoreEntryComp.create(sec.getWrapper(), null, preferLarge); + } + + var enabled = StoreToggleComp.>enableToggle( + null, sec, s -> s.getState().isEnabled(), (s, aBoolean) -> { + var state = s.getState().toBuilder().enabled(aBoolean).build(); + s.setState(state); + }); + + var e = sec.getWrapper().getEntry(); + var parent = DataStorage.get().getDefaultDisplayParent(e); + if (parent.isPresent()) { + var parentWrapper = StoreViewState.get().getEntryWrapper(parent.get()); + // Disable selection if parent is already made enabled + enabled.setCustomVisibility(BindingsHelper.map(parentWrapper.getPersistentState(), o -> { + EnabledStoreState state = (EnabledStoreState) o; + return !state.isEnabled(); + })); + } + + return StoreEntryComp.create(sec.getWrapper(), 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 new file mode 100644 index 00000000..d67a9b20 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/ext/EnabledStoreProvider.java @@ -0,0 +1,25 @@ +package io.xpipe.app.ext; + +import io.xpipe.app.comp.base.StoreToggleComp; +import io.xpipe.app.comp.store.StoreEntryComp; +import io.xpipe.app.comp.store.StoreSection; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.core.store.EnabledStoreState; +import io.xpipe.core.store.StatefulDataStore; + +public interface EnabledStoreProvider extends DataStoreProvider { + + @Override + public default StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) { + if (sec.getWrapper().getValidity().getValue() != DataStoreEntry.Validity.COMPLETE) { + return StoreEntryComp.create(sec.getWrapper(), null, preferLarge); + } + + var enabled = StoreToggleComp.>enableToggle( + null, sec, s -> s.getState().isEnabled(), (s, aBoolean) -> { + var state = s.getState().toBuilder().enabled(aBoolean).build(); + s.setState(state); + }); + return StoreEntryComp.create(sec.getWrapper(), enabled, preferLarge); + } +} diff --git a/app/src/main/java/io/xpipe/app/ext/ExtensionException.java b/app/src/main/java/io/xpipe/app/ext/ExtensionException.java index bd7b0c23..e26db767 100644 --- a/app/src/main/java/io/xpipe/app/ext/ExtensionException.java +++ b/app/src/main/java/io/xpipe/app/ext/ExtensionException.java @@ -21,6 +21,6 @@ public class ExtensionException extends RuntimeException { } public static ExtensionException corrupt(String message) { - return new ExtensionException(message + ". Is the installation corrupt?"); + return new ExtensionException(message + ". Is the installation data corrupt?"); } } 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 1a0e4244..720da111 100644 --- a/app/src/main/java/io/xpipe/app/ext/SingletonSessionStoreProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/SingletonSessionStoreProvider.java @@ -6,12 +6,14 @@ 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.fxcomps.Comp; +import io.xpipe.app.fxcomps.util.LabelGraphic; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.store.SingletonSessionStore; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ObservableBooleanValue; +import javafx.beans.value.ObservableValue; public interface SingletonSessionStoreProvider extends DataStoreProvider { @@ -38,7 +40,9 @@ public interface SingletonSessionStoreProvider extends DataStoreProvider { enabled.set(s.isSessionEnabled()); }); - var t = new StoreToggleComp(null, sec, enabled, aBoolean -> { + ObservableValue g = enabled.map(aBoolean -> aBoolean ? + new LabelGraphic.IconGraphic("mdi2c-circle-slice-8") : new LabelGraphic.IconGraphic("mdi2p-power")); + var t = new StoreToggleComp(null, g, sec, enabled, aBoolean -> { SingletonSessionStore s = sec.getWrapper().getEntry().getStore().asNeeded(); if (s.isSessionEnabled() != aBoolean) { ThreadHelper.runFailableAsync(() -> { @@ -50,6 +54,7 @@ public interface SingletonSessionStoreProvider extends DataStoreProvider { }); } }); + t.tooltipKey("enabled"); return t; } 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 94c05144..438247b0 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 @@ -53,12 +53,12 @@ public class IconButtonComp extends Comp> { }); // fi.iconColorProperty().bind(button.textFillProperty()); button.setGraphic(fi); - button.setOnAction(e -> { - if (listener != null) { + if (listener != null) { + button.setOnAction(e -> { listener.run(); e.consume(); - } - }); + }); + } button.getStyleClass().add("icon-button-comp"); return new SimpleCompStructure<>(button); } 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 c461917c..f85aeda6 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 @@ -79,7 +79,7 @@ public class StoreCategoryComp extends SimpleComp { })); var shownList = new DerivedObservableList<>(category.getContainedEntries(), true).filtered( storeEntryWrapper -> { - return storeEntryWrapper.shouldShow( + return storeEntryWrapper.matchesFilter( StoreViewState.get().getFilterString().getValue()); }, StoreViewState.get().getFilterString()).getList(); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/util/DerivedObservableList.java b/app/src/main/java/io/xpipe/app/fxcomps/util/DerivedObservableList.java index 166bf3ef..cd810258 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/util/DerivedObservableList.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/util/DerivedObservableList.java @@ -129,9 +129,17 @@ public class DerivedObservableList { } public DerivedObservableList mapped(Function map) { + var cache = new HashMap(); var l1 = this.createNewDerived(); Runnable runnable = () -> { - l1.setContent(list.stream().map(map).toList()); + cache.keySet().removeIf(t -> !getList().contains(t)); + l1.setContent(list.stream().map(v -> { + if (!cache.containsKey(v)) { + cache.put(v, map.apply(v)); + } + + return cache.get(v); + }).toList()); }; runnable.run(); list.addListener((ListChangeListener) c -> { 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 new file mode 100644 index 00000000..475300f1 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/fxcomps/util/LabelGraphic.java @@ -0,0 +1,42 @@ +package io.xpipe.app.fxcomps.util; + +import io.xpipe.app.fxcomps.Comp; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; +import javafx.scene.Node; +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.kordamp.ikonli.javafx.FontIcon; + +public abstract class LabelGraphic { + + public static ObservableValue fixedIcon(String icon) { + return new SimpleObjectProperty<>(new IconGraphic(icon)); + } + + public abstract Node createGraphicNode(); + + @Value + @EqualsAndHashCode(callSuper = true) + public static class IconGraphic extends LabelGraphic { + + String icon; + + @Override + public Node createGraphicNode() { + return new FontIcon(icon); + } + } + + @Value + @EqualsAndHashCode(callSuper = true) + public static class CompGraphic extends LabelGraphic { + + Comp comp; + + @Override + public Node createGraphicNode() { + return comp.createRegion(); + } + } +} 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 b47f84e6..5ac65270 100644 --- a/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java +++ b/app/src/main/java/io/xpipe/app/launcher/LauncherCommand.java @@ -1,5 +1,6 @@ package io.xpipe.app.launcher; +import io.xpipe.app.beacon.AppBeaconServer; import io.xpipe.app.core.AppDataLock; import io.xpipe.app.core.AppLogs; import io.xpipe.app.core.AppProperties; @@ -9,13 +10,13 @@ import io.xpipe.app.issue.LogErrorHandler; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.ThreadHelper; -import io.xpipe.beacon.BeaconServer; -import io.xpipe.beacon.exchange.FocusExchange; -import io.xpipe.beacon.exchange.OpenExchange; +import io.xpipe.beacon.BeaconClient; +import io.xpipe.beacon.BeaconClientInformation; +import io.xpipe.beacon.api.DaemonFocusExchange; +import io.xpipe.beacon.api.DaemonOpenExchange; import io.xpipe.core.process.OsType; import io.xpipe.core.util.XPipeDaemonMode; import io.xpipe.core.util.XPipeInstallation; - import lombok.SneakyThrows; import picocli.CommandLine; @@ -81,26 +82,25 @@ public class LauncherCommand implements Callable { private void checkStart() { try { - if (BeaconServer.isReachable()) { - try (var con = new LauncherConnection()) { - con.constructSocket(); - con.performSimpleExchange(FocusExchange.Request.builder() - .mode(getEffectiveMode()) - .build()); + var port = AppBeaconServer.get().getPort(); + var client = BeaconClient.tryEstablishConnection(port, BeaconClientInformation.Daemon.builder().build()); + if (client.isPresent()) { + client.get().performRequest(DaemonFocusExchange.Request.builder().mode(getEffectiveMode()).build()); if (!inputs.isEmpty()) { - con.performSimpleExchange( - OpenExchange.Request.builder().arguments(inputs).build()); + client.get().performRequest( + DaemonOpenExchange.Request.builder().arguments(inputs).build()); } if (OsType.getLocal().equals(OsType.MACOS)) { Desktop.getDesktop().setOpenURIHandler(e -> { - con.performSimpleExchange(OpenExchange.Request.builder() - .arguments(List.of(e.getURI().toString())) - .build()); + try { + client.get().performRequest(DaemonOpenExchange.Request.builder().arguments(List.of(e.getURI().toString())).build()); + } catch (Exception ex) { + ErrorEvent.fromThrowable(ex).expected().omit().handle(); + } }); ThreadHelper.sleep(1000); } - } TrackEvent.info("Another instance is already running on this port. Quitting ..."); OperationMode.halt(1); } diff --git a/app/src/main/java/io/xpipe/app/launcher/LauncherConnection.java b/app/src/main/java/io/xpipe/app/launcher/LauncherConnection.java deleted file mode 100644 index fdbe0a25..00000000 --- a/app/src/main/java/io/xpipe/app/launcher/LauncherConnection.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.xpipe.app.launcher; - -import io.xpipe.beacon.BeaconClient; -import io.xpipe.beacon.BeaconConnection; -import io.xpipe.beacon.BeaconException; - -public class LauncherConnection extends BeaconConnection { - - @Override - protected void constructSocket() { - try { - beaconClient = BeaconClient.establishConnection( - BeaconClient.DaemonInformation.builder().build()); - } catch (Exception ex) { - throw new BeaconException("Unable to connect to running xpipe daemon", ex); - } - } -} diff --git a/app/src/main/java/io/xpipe/app/launcher/LauncherInput.java b/app/src/main/java/io/xpipe/app/launcher/LauncherInput.java index adf5d47a..070cdc21 100644 --- a/app/src/main/java/io/xpipe/app/launcher/LauncherInput.java +++ b/app/src/main/java/io/xpipe/app/launcher/LauncherInput.java @@ -2,7 +2,6 @@ package io.xpipe.app.launcher; import io.xpipe.app.browser.session.BrowserSessionModel; import io.xpipe.app.core.AppLayoutModel; -import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.TrackEvent; @@ -34,17 +33,13 @@ public abstract class LauncherInput { } }); - var requiresPlatform = all.stream().anyMatch(launcherInput -> launcherInput.requiresJavaFXPlatform()); - if (requiresPlatform) { - OperationMode.switchToSyncIfPossible(OperationMode.GUI); - } - var hasGui = OperationMode.get() == OperationMode.GUI; +// var requiresPlatform = all.stream().anyMatch(launcherInput -> launcherInput.requiresJavaFXPlatform()); +// if (requiresPlatform) { +// OperationMode.switchToSyncIfPossible(OperationMode.GUI); +// } +// var hasGui = OperationMode.get() == OperationMode.GUI; all.forEach(launcherInput -> { - if (!hasGui && launcherInput.requiresJavaFXPlatform()) { - return; - } - try { launcherInput.execute(); } catch (Exception e) { @@ -102,11 +97,6 @@ public abstract class LauncherInput { Path file; - @Override - public boolean requiresJavaFXPlatform() { - return true; - } - @Override public void execute() { if (!Files.exists(file)) { 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 2765b7b1..a2e7e358 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java @@ -14,14 +14,13 @@ 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 io.xpipe.core.util.XPipeInstallation; 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; @@ -121,6 +120,25 @@ public class AppPrefs { private final StringProperty lockCrypt = mapVaultSpecific(new SimpleStringProperty(), "workspaceLock", String.class); + final Property httpServerPort = + mapVaultSpecific(new SimpleObjectProperty<>(XPipeInstallation.getDefaultBeaconPort()), "httpServerPort", Integer.class); + final StringProperty apiKey = + mapVaultSpecific(new SimpleStringProperty(UUID.randomUUID().toString()), "apiKey", String.class); + final BooleanProperty disableApiAuthentication = + mapVaultSpecific(new SimpleBooleanProperty(false), "disableApiAuthentication", Boolean.class); + + public ObservableValue httpServerPort() { + return httpServerPort; + } + + public ObservableStringValue apiKey() { + return apiKey; + } + + public ObservableBooleanValue disableApiAuthentication() { + return disableApiAuthentication; + } + private final IntegerProperty editorReloadTimeout = map(new SimpleIntegerProperty(1000), "editorReloadTimeout", Integer.class); private final BooleanProperty confirmDeletions = @@ -153,6 +171,7 @@ public class AppPrefs { new SshCategory(), new LocalShellCategory(), new SecurityCategory(), + new HttpApiCategory(), new TroubleshootCategory(), new DeveloperCategory()) .filter(appPrefsCategory -> appPrefsCategory.show()) @@ -435,12 +454,8 @@ public class AppPrefs { } public void initDefaultValues() { - if (externalEditor.get() == null) { - ExternalEditorType.detectDefault(); - } - + externalEditor.setValue(ExternalEditorType.detectDefault(externalEditor.get())); terminalType.set(ExternalTerminalType.determineDefault(terminalType.get())); - if (rdpClientType.get() == null) { rdpClientType.setValue(ExternalRdpClientType.determineDefault()); } 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 58ebf9b1..e992eebb 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java @@ -45,16 +45,17 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue { @Override public boolean isAvailable() { try (ShellControl pc = LocalShell.getShell().start()) { - return pc.command(String.format( + var out = pc.command(String.format( "mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications", applicationName)) - .executeAndCheck(); + .readStdoutIfPossible(); + return out.isPresent() && !out.get().isBlank(); } catch (Exception e) { ErrorEvent.fromThrowable(e).handle(); return false; } } - + @Override public boolean isSelectable() { return OsType.getLocal().equals(OsType.MACOS); 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 dce1a563..75098026 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java @@ -145,43 +145,34 @@ public interface ExternalEditorType extends PrefsChoiceValue { }) .get(); - static void detectDefault() { - var typeProperty = AppPrefs.get().externalEditor; - var customProperty = AppPrefs.get().customEditorCommand; + static ExternalEditorType detectDefault(ExternalEditorType existing) { + // Verify that our selection is still valid + if (existing != null && existing.isAvailable()) { + return existing; + } + if (OsType.getLocal().equals(OsType.WINDOWS)) { - typeProperty.set(WINDOWS_EDITORS.stream() + return WINDOWS_EDITORS.stream() .filter(PrefsChoiceValue::isAvailable) .findFirst() - .orElse(null)); + .orElse(null); } if (OsType.getLocal().equals(OsType.LINUX)) { - var env = System.getenv("VISUAL"); - if (env != null) { - var found = LINUX_EDITORS.stream() - .filter(externalEditorType -> externalEditorType.executable.equalsIgnoreCase(env)) - .findFirst() - .orElse(null); - if (found == null) { - typeProperty.set(CUSTOM); - customProperty.set(env); - } else { - typeProperty.set(found); - } - } else { - typeProperty.set(LINUX_EDITORS.stream() - .filter(ExternalApplicationType.PathApplication::isAvailable) - .findFirst() - .orElse(null)); - } + return LINUX_EDITORS.stream() + .filter(ExternalApplicationType.PathApplication::isAvailable) + .findFirst() + .orElse(null); } if (OsType.getLocal().equals(OsType.MACOS)) { - typeProperty.set(MACOS_EDITORS.stream() + return MACOS_EDITORS.stream() .filter(PrefsChoiceValue::isAvailable) .findFirst() - .orElse(null)); + .orElse(null); } + + return null; } void launch(Path file) throws Exception; @@ -194,10 +185,10 @@ public interface ExternalEditorType extends PrefsChoiceValue { @Override public void launch(Path file) throws Exception { - ExternalApplicationHelper.startAsync(CommandBuilder.of() - .add("open", "-a") - .addQuoted(applicationName) - .addFile(file.toString())); + try (var sc = LocalShell.getShell().start()) { + sc.executeSimpleCommand(CommandBuilder.of() + .add("open", "-a").addQuoted(applicationName).addFile(file.toString())); + } } } diff --git a/app/src/main/java/io/xpipe/app/prefs/HttpApiCategory.java b/app/src/main/java/io/xpipe/app/prefs/HttpApiCategory.java new file mode 100644 index 00000000..94bcf2ba --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/HttpApiCategory.java @@ -0,0 +1,30 @@ +package io.xpipe.app.prefs; + +import io.xpipe.app.beacon.AppBeaconServer; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.util.OptionsBuilder; + +public class HttpApiCategory extends AppPrefsCategory { + + @Override + protected String getId() { + return "httpApi"; + } + + @Override + protected Comp create() { + var prefs = AppPrefs.get(); + return new OptionsBuilder() + .addTitle("httpServerConfiguration") + .sub(new OptionsBuilder() + .nameAndDescription("httpServerPort") + .addInteger(prefs.httpServerPort) + .disable(AppBeaconServer.get().isPropertyPort()) + .nameAndDescription("apiKey") + .addString(prefs.apiKey) + .nameAndDescription("disableApiAuthentication") + .addToggle(prefs.disableApiAuthentication) + ) + .buildComp(); + } +} 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 62431eff..f88ef002 100644 --- a/app/src/main/java/io/xpipe/app/prefs/SyncCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/SyncCategory.java @@ -2,6 +2,7 @@ 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; @@ -19,8 +20,10 @@ public class SyncCategory extends AppPrefsCategory { var builder = new OptionsBuilder(); builder.addTitle("sync") .sub(new OptionsBuilder() - .nameAndDescription("enableGitStorage") + .name("enableGitStorage") + .description(AppProperties.get().isStaging() ? "enableGitStoragePtbDisabled" : "enableGitStorage") .addToggle(prefs.enableGitStorage) + .disable(AppProperties.get().isStaging()) .nameAndDescription("storageGitRemote") .addString(prefs.storageGitRemote, true) .disable(prefs.enableGitStorage.not()) 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 dbdac50c..c1712a54 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java @@ -5,11 +5,12 @@ import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.FixedHierarchyStore; import io.xpipe.app.util.ThreadHelper; -import io.xpipe.core.store.*; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.store.FixedChildStore; +import io.xpipe.core.store.LocalStore; +import io.xpipe.core.store.StorePath; import io.xpipe.core.util.UuidHelper; - import javafx.util.Pair; - import lombok.Getter; import lombok.NonNull; import lombok.Setter; @@ -20,7 +21,6 @@ import java.time.Instant; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -61,6 +61,8 @@ public abstract class DataStorage { @Setter protected DataStoreCategory selectedCategory; + private final Map storeEntryMapCache = Collections.synchronizedMap(new IdentityHashMap<>()); + public DataStorage() { var prefsDir = AppPrefs.get().storageDirectory().getValue(); this.dir = !Files.exists(prefsDir) || !Files.isDirectory(prefsDir) ? AppPrefs.DEFAULT_STORAGE_DIR : prefsDir; @@ -260,16 +262,10 @@ public abstract class DataStorage { return true; } - protected void refreshValidities(boolean makeValid) { - var changed = new AtomicBoolean(false); - do { - changed.set(false); - storeEntries.keySet().forEach(dataStoreEntry -> { - if (makeValid ? dataStoreEntry.tryMakeValid() : dataStoreEntry.tryMakeInvalid()) { - changed.set(true); - } - }); - } while (changed.get()); + protected void refreshEntries() { + storeEntries.keySet().forEach(dataStoreEntry -> { + dataStoreEntry.refreshStore(); + }); } public void updateEntry(DataStoreEntry entry, DataStoreEntry newEntry) { @@ -299,8 +295,7 @@ public abstract class DataStorage { var toAdd = Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new); listeners.forEach(storageListener -> storageListener.onStoreAdd(toAdd)); } - refreshValidities(true); - + refreshEntries(); saveAsync(); } @@ -345,21 +340,21 @@ public abstract class DataStorage { return false; } - e.setInRefresh(true); + e.incrementBusyCounter(); List> newChildren; try { newChildren = ((FixedHierarchyStore) (e.getStore())).listChildren(e); - e.setInRefresh(false); } catch (Exception ex) { - e.setInRefresh(false); ErrorEvent.fromThrowable(ex).handle(); return false; + } finally { + e.decrementBusyCounter(); } - var oldChildren = getStoreEntries().stream() - .filter(other -> e.equals(getDefaultDisplayParent(other).orElse(null))) - .toList(); + + var oldChildren = getStoreChildren(e); var toRemove = oldChildren.stream() + .filter(oc -> oc.getStore() instanceof FixedChildStore) .filter(oc -> { var oid = ((FixedChildStore) oc.getStore()).getFixedId(); if (oid.isEmpty()) { @@ -375,6 +370,10 @@ public abstract class DataStorage { .toList(); var toAdd = newChildren.stream() .filter(nc -> { + if (nc.getStore() == null) { + return false; + } + var nid = nc.getStore().getFixedId(); // These can't be automatically generated if (nid.isEmpty()) { @@ -382,6 +381,7 @@ public abstract class DataStorage { } return oldChildren.stream() + .filter(oc -> oc.getStore() instanceof FixedChildStore) .filter(oc -> ((FixedChildStore) oc.getStore()) .getFixedId() .isPresent()) @@ -394,6 +394,7 @@ public abstract class DataStorage { }) .toList(); var toUpdate = oldChildren.stream() + .filter(oc -> oc.getStore() instanceof FixedChildStore) .map(oc -> { var oid = ((FixedChildStore) oc.getStore()).getFixedId(); if (oid.isEmpty()) { @@ -445,7 +446,10 @@ public abstract class DataStorage { } } }); + refreshEntries(); saveAsync(); + toAdd.forEach(dataStoreEntryRef -> dataStoreEntryRef.get().getProvider().onChildrenRefresh(dataStoreEntryRef.getEntry())); + toUpdate.forEach(dataStoreEntryRef -> dataStoreEntryRef.getKey().getProvider().onChildrenRefresh(dataStoreEntryRef.getKey())); return !newChildren.isEmpty(); } @@ -458,7 +462,7 @@ public abstract class DataStorage { c.forEach(entry -> entry.finalizeEntry()); this.storeEntriesSet.removeAll(c); this.listeners.forEach(l -> l.onStoreRemove(c.toArray(DataStoreEntry[]::new))); - refreshValidities(false); + refreshEntries(); saveAsync(); } @@ -477,7 +481,7 @@ public abstract class DataStorage { toDelete.forEach(entry -> entry.finalizeEntry()); toDelete.forEach(this.storeEntriesSet::remove); this.listeners.forEach(l -> l.onStoreRemove(toDelete.toArray(DataStoreEntry[]::new))); - refreshValidities(false); + refreshEntries(); saveAsync(); } @@ -532,7 +536,7 @@ public abstract class DataStorage { this.listeners.forEach(l -> l.onStoreAdd(e)); e.initializeEntry(); - refreshValidities(true); + e.refreshStore(); return e; } @@ -566,11 +570,13 @@ public abstract class DataStorage { p.setChildrenCache(null); }); } + for (DataStoreEntry e : toAdd) { + e.refreshStore(); + } this.listeners.forEach(l -> l.onStoreAdd(toAdd.toArray(DataStoreEntry[]::new))); for (DataStoreEntry e : toAdd) { e.initializeEntry(); } - refreshValidities(true); saveAsync(); } @@ -598,7 +604,7 @@ public abstract class DataStorage { this.storeEntries.remove(store); getDefaultDisplayParent(store).ifPresent(p -> p.setChildrenCache(null)); this.listeners.forEach(l -> l.onStoreRemove(store)); - refreshValidities(false); + refreshEntries(); saveAsync(); } @@ -734,6 +740,22 @@ public abstract class DataStorage { return children; } + public List getCategoryParentHierarchy(DataStoreCategory cat) { + var es = new ArrayList(); + es.add(cat); + + DataStoreCategory current = cat; + while ((current = getStoreCategoryIfPresent(current.getParentCategory()).orElse(null)) != null) { + if (es.contains(current)) { + break; + } + + es.addFirst(current); + } + + return es; + } + public List getStoreParentHierarchy(DataStoreEntry entry) { var es = new ArrayList(); es.add(entry); @@ -750,34 +772,17 @@ public abstract class DataStorage { return es; } - public DataStoreId getId(DataStoreEntry entry) { - return DataStoreId.create(getStoreParentHierarchy(entry).stream() + public StorePath getStorePath(DataStoreEntry entry) { + return StorePath.create(getStoreParentHierarchy(entry).stream() .filter(e -> !(e.getStore() instanceof LocalStore)) - .map(e -> e.getName().replaceAll(":", "_")) + .map(e -> e.getName().toLowerCase().replaceAll("/", "_")) .toArray(String[]::new)); } - public Optional getStoreEntryIfPresent(@NonNull DataStoreId id) { - var current = getStoreEntryIfPresent(id.getNames().getFirst()); - if (current.isPresent()) { - for (int i = 1; i < id.getNames().size(); i++) { - var children = getStoreChildren(current.get()); - int finalI = i; - current = children.stream() - .filter(dataStoreEntry -> dataStoreEntry - .getName() - .equalsIgnoreCase(id.getNames().get(finalI))) - .findFirst(); - if (current.isEmpty()) { - break; - } - } - - if (current.isPresent()) { - return current; - } - } - return Optional.empty(); + public StorePath getStorePath(DataStoreCategory entry) { + return StorePath.create(getCategoryParentHierarchy(entry).stream() + .map(e -> e.getName().toLowerCase().replaceAll("/", "_")) + .toArray(String[]::new)); } public Optional getStoreEntryInProgressIfPresent(@NonNull DataStore store) { @@ -787,12 +792,29 @@ public abstract class DataStorage { } public Optional getStoreEntryIfPresent(@NonNull DataStore store, boolean identityOnly) { - return storeEntriesSet.stream() + if (identityOnly) { + synchronized (storeEntryMapCache) { + var found = storeEntryMapCache.get(store); + if (found != null) { + return Optional.of(found); + } + } + } + + var found = storeEntriesSet.stream() .filter(n -> n.getStore() == store || (!identityOnly && (n.getStore() != null && Objects.equals( store.getClass(), n.getStore().getClass()) && store.equals(n.getStore())))) .findFirst(); + if (found.isPresent()) { + if (identityOnly) { + synchronized (storeEntryMapCache) { + storeEntryMapCache.put(store, found.get()); + } + } + } + return found; } public DataStoreCategory getRootCategory(DataStoreCategory category) { @@ -854,12 +876,22 @@ public abstract class DataStorage { } public DataStoreEntry getOrCreateNewSyntheticEntry(DataStoreEntry parent, String name, DataStore store) { + var forStoreIdentity = getStoreEntryIfPresent(store, true); + if (forStoreIdentity.isPresent()) { + return forStoreIdentity.get(); + } + var uuid = UuidHelper.generateFromObject(parent.getUuid(), name); var found = getStoreEntryIfPresent(uuid); if (found.isPresent()) { return found.get(); } + var forStore = getStoreEntryIfPresent(store, false); + if (forStore.isPresent()) { + return forStore.get(); + } + return DataStoreEntry.createNew(uuid, parent.getCategoryUuid(), name, store); } 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 0b705df8..f4455869 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java @@ -21,6 +21,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -46,8 +47,7 @@ public class DataStoreEntry extends StorageElement { @NonFinal boolean expanded; - @NonFinal - boolean inRefresh; + AtomicInteger busyCounter = new AtomicInteger(); @Getter @NonFinal @@ -323,14 +323,21 @@ public class DataStoreEntry extends StorageElement { return getName(); } - public void setInRefresh(boolean newRefresh) { - var changed = inRefresh != newRefresh; - if (changed) { - this.inRefresh = newRefresh; + public void incrementBusyCounter() { + var r = busyCounter.incrementAndGet() == 1; + if (r) { notifyUpdate(false, false); } } + public boolean decrementBusyCounter() { + var r = busyCounter.decrementAndGet() == 0; + if (r) { + notifyUpdate(false, false); + } + return r; + } + public DataStoreEntryRef ref() { return new DataStoreEntryRef<>(this); } @@ -503,7 +510,7 @@ public class DataStoreEntry extends StorageElement { try { store.checkComplete(); - setInRefresh(true); + incrementBusyCounter(); if (store instanceof ValidatableStore l) { l.validate(); } else if (store instanceof FixedHierarchyStore h) { @@ -512,32 +519,27 @@ public class DataStoreEntry extends StorageElement { .collect(Collectors.toSet()); } } finally { - setInRefresh(false); + decrementBusyCounter(); } } - public boolean tryMakeValid() { + public void refreshStore() { if (validity == Validity.LOAD_FAILED) { - return false; - } - - var complete = validity == Validity.COMPLETE; - if (complete) { - return false; + return; } var newStore = DataStorageParser.storeFromNode(storeNode); if (newStore == null) { store = null; validity = Validity.LOAD_FAILED; - return true; + return; } var newComplete = newStore.isComplete(); if (!newComplete) { validity = Validity.INCOMPLETE; store = newStore; - return false; + return; } if (!newStore.equals(store)) { @@ -546,52 +548,19 @@ public class DataStoreEntry extends StorageElement { validity = Validity.COMPLETE; // Don't count this as modification as this is done always notifyUpdate(false, false); - return true; - } - - public boolean tryMakeInvalid() { - if (validity == Validity.LOAD_FAILED) { - return false; - } - - if (validity == Validity.INCOMPLETE) { - return false; - } - - var newStore = DataStorageParser.storeFromNode(storeNode); - if (newStore == null) { - store = null; - validity = Validity.LOAD_FAILED; - return true; - } - - var newComplete = newStore.isComplete(); - if (newComplete) { - validity = Validity.COMPLETE; - store = newStore; - return false; - } - - if (!newStore.equals(store)) { - store = newStore; - } - validity = Validity.INCOMPLETE; - notifyUpdate(false, false); - return true; } @SneakyThrows public void initializeEntry() { if (store instanceof ExpandedLifecycleStore lifecycleStore) { try { - inRefresh = true; + incrementBusyCounter(); notifyUpdate(false, false); lifecycleStore.initializeValidate(); - inRefresh = false; } catch (Exception e) { - inRefresh = false; ErrorEvent.fromThrowable(e).handle(); } finally { + decrementBusyCounter(); notifyUpdate(false, false); } } @@ -601,12 +570,13 @@ public class DataStoreEntry extends StorageElement { public void finalizeEntry() { if (store instanceof ExpandedLifecycleStore lifecycleStore) { try { - inRefresh = true; + incrementBusyCounter(); notifyUpdate(false, false); lifecycleStore.finalizeValidate(); } catch (Exception e) { ErrorEvent.fromThrowable(e).handle(); } finally { + decrementBusyCounter(); notifyUpdate(false, false); } } diff --git a/app/src/main/java/io/xpipe/app/storage/ImpersistentStorage.java b/app/src/main/java/io/xpipe/app/storage/ImpersistentStorage.java index e352def6..8d11a11d 100644 --- a/app/src/main/java/io/xpipe/app/storage/ImpersistentStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/ImpersistentStorage.java @@ -1,13 +1,8 @@ package io.xpipe.app.storage; import io.xpipe.app.comp.store.StoreSortMode; -import io.xpipe.app.issue.ErrorEvent; -import io.xpipe.app.issue.TrackEvent; import io.xpipe.core.store.LocalStore; -import org.apache.commons.io.FileUtils; - -import java.nio.file.Files; import java.time.Instant; import java.util.UUID; @@ -20,22 +15,17 @@ public class ImpersistentStorage extends DataStorage { @Override public void load() { - var storesDir = getStoresDir(); - var categoriesDir = getCategoriesDir(); - { var cat = DataStoreCategory.createNew(null, ALL_CONNECTIONS_CATEGORY_UUID, "All connections"); - cat.setDirectory(categoriesDir.resolve(ALL_CONNECTIONS_CATEGORY_UUID.toString())); storeCategories.add(cat); } { var cat = DataStoreCategory.createNew(null, ALL_SCRIPTS_CATEGORY_UUID, "All scripts"); - cat.setDirectory(categoriesDir.resolve(ALL_SCRIPTS_CATEGORY_UUID.toString())); storeCategories.add(cat); } { var cat = new DataStoreCategory( - categoriesDir.resolve(DEFAULT_CATEGORY_UUID.toString()), + null, DEFAULT_CATEGORY_UUID, "Default", Instant.now(), @@ -50,7 +40,6 @@ public class ImpersistentStorage extends DataStorage { var e = DataStoreEntry.createNew( LOCAL_ID, DataStorage.DEFAULT_CATEGORY_UUID, "Local Machine", new LocalStore()); - e.setDirectory(getStoresDir().resolve(LOCAL_ID.toString())); e.setConfiguration( StorageElement.Configuration.builder().deletable(false).build()); storeEntries.put(e, e); @@ -58,18 +47,7 @@ public class ImpersistentStorage extends DataStorage { } @Override - public synchronized void save(boolean dispose) { - var storesDir = getStoresDir(); - - TrackEvent.info("Storage persistence is disabled. Deleting storage contents ..."); - try { - if (Files.exists(storesDir)) { - FileUtils.cleanDirectory(storesDir.toFile()); - } - } catch (Exception ex) { - ErrorEvent.fromThrowable(ex).build().handle(); - } - } + public synchronized void save(boolean dispose) {} @Override public boolean supportsSharing() { 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 34958fba..1a64ee96 100644 --- a/app/src/main/java/io/xpipe/app/storage/StandardStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/StandardStorage.java @@ -1,11 +1,11 @@ package io.xpipe.app.storage; +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 lombok.Getter; import org.apache.commons.io.FileUtils; @@ -197,14 +197,15 @@ public class StandardStorage extends DataStorage { local.setColor(DataStoreColor.BLUE); } - refreshValidities(true); + callProviders(); + refreshEntries(); storeEntriesSet.forEach(entry -> { var syntheticParent = getSyntheticParent(entry); syntheticParent.ifPresent(entry1 -> { addStoreEntryIfNotPresent(entry1); }); }); - refreshValidities(true); + refreshEntries(); // Save to apply changes if (!hasFixedLocal) { @@ -226,6 +227,16 @@ public class StandardStorage extends DataStorage { this.gitStorageHandler.afterStorageLoad(); } + private void callProviders() { + DataStorageExtensionProvider.getAll().forEach(p -> { + try { + p.storageInit(); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).omit().handle(); + } + }); + } + public void save(boolean dispose) { try { // If another save operation is in progress, we have to wait on dispose diff --git a/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java index 5fa67647..e281f21a 100644 --- a/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java @@ -238,6 +238,35 @@ public interface ExternalTerminalType extends PrefsChoiceValue { .addFile(configuration.getScriptFile()); } }; + ExternalTerminalType FOOT = new SimplePathType("app.foot", "foot", true) { + @Override + public String getWebsite() { + return "https://codeberg.org/dnkl/foot"; + } + + @Override + public boolean supportsTabs() { + return false; + } + + @Override + public boolean isRecommended() { + return false; + } + + @Override + public boolean supportsColoredTitle() { + return true; + } + + @Override + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + return CommandBuilder.of() + .add("--title") + .addQuoted(configuration.getColoredTitle()) + .addFile(configuration.getScriptFile()); + } + }; ExternalTerminalType ELEMENTARY = new SimplePathType("app.elementaryTerminal", "io.elementary.terminal", true) { @Override @@ -262,7 +291,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { @Override protected CommandBuilder toCommand(LaunchConfiguration configuration) { - return CommandBuilder.of().add("--new-tab").add("-e").addFile(configuration.getColoredTitle()); + return CommandBuilder.of().add("--new-tab").add("-e").addFile(configuration.getScriptFile()); } }; ExternalTerminalType TILIX = new SimplePathType("app.tilix", "tilix", true) { @@ -514,17 +543,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue { @Override public void launch(LaunchConfiguration configuration) throws Exception { - try (ShellControl pc = LocalShell.getShell()) { - var suffix = "\"" + configuration.getScriptFile().toString().replaceAll("\"", "\\\\\"") + "\""; - pc.osascriptCommand(String.format( - """ - activate application "Terminal" - delay 1 - tell app "Terminal" to do script %s - """, - suffix)) - .execute(); - } + LocalShell.getShell() + .executeSimpleCommand(CommandBuilder.of() + .add("open", "-a") + .addQuoted("Terminal.app") + .addFile(configuration.getScriptFile())); } }; ExternalTerminalType ITERM2 = new MacOsType("app.iterm2", "iTerm") { @@ -550,26 +573,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue { @Override public void launch(LaunchConfiguration configuration) throws Exception { - try (ShellControl pc = LocalShell.getShell()) { - pc.osascriptCommand(String.format( - """ - if application "iTerm" is not running then - launch application "iTerm" - delay 1 - tell application "iTerm" - tell current tab of current window - close - end tell - end tell - end if - tell application "iTerm" - activate - create window with default profile command "%s" - end tell - """, - configuration.getScriptFile().toString().replaceAll("\"", "\\\\\""))) - .execute(); - } + LocalShell.getShell() + .executeSimpleCommand(CommandBuilder.of() + .add("open", "-a") + .addQuoted("iTerm.app") + .addFile(configuration.getScriptFile())); } }; ExternalTerminalType WARP = new MacOsType("app.warp", "Warp") { @@ -662,6 +670,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { TILDA, XTERM, DEEPIN_TERMINAL, + FOOT, Q_TERMINAL); List MACOS_TERMINALS = List.of( KittyTerminalType.KITTY_MACOS, @@ -705,7 +714,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue { return ExternalTerminalType.POWERSHELL; } - if (existing != null) { + // Verify that our selection is still valid + if (existing != null && existing.isAvailable()) { return existing; } diff --git a/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java index 8bf48d1a..f63d33be 100644 --- a/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java @@ -1,9 +1,14 @@ package io.xpipe.app.terminal; +import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.ExternalApplicationHelper; +import io.xpipe.app.prefs.ExternalApplicationType; import io.xpipe.app.util.LocalShell; +import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.WindowsRegistry; import io.xpipe.core.process.CommandBuilder; +import io.xpipe.core.process.OsType; +import io.xpipe.core.process.ShellControl; import java.nio.file.Path; import java.util.Optional; @@ -26,7 +31,7 @@ public interface WezTerminalType extends ExternalTerminalType { @Override default boolean isRecommended() { - return false; + return OsType.getLocal() != OsType.WINDOWS; } @Override @@ -51,25 +56,62 @@ public interface WezTerminalType extends ExternalTerminalType { @Override protected Optional determineInstallation() { - Optional launcherDir; - launcherDir = WindowsRegistry.local().readValue( - WindowsRegistry.HKEY_LOCAL_MACHINE, - "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{BCF6F0DA-5B9A-408D-8562-F680AE6E1EAF}_is1", - "InstallLocation") - .map(p -> p + "\\wezterm-gui.exe"); - return launcherDir.map(Path::of); + try { + var foundKey = WindowsRegistry.local().findKeyForEqualValueMatchRecursive(WindowsRegistry.HKEY_LOCAL_MACHINE, + "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "http://wezfurlong.org/wezterm"); + if (foundKey.isPresent()) { + var installKey = WindowsRegistry.local().readValue( + foundKey.get().getHkey(), + foundKey.get().getKey(), + "InstallLocation"); + if (installKey.isPresent()) { + return installKey.map(p -> p + "\\wezterm-gui.exe").map(Path::of); + } + } + } catch (Exception ex) { + ErrorEvent.fromThrowable(ex).omit().handle(); + } + + try (ShellControl pc = LocalShell.getShell()) { + if (pc.executeSimpleBooleanCommand(pc.getShellDialect().getWhichCommand("wezterm-gui"))) { + return Optional.of(Path.of("wezterm-gui")); + } + } catch (Exception e) { + ErrorEvent.fromThrowable(e).omit().handle(); + } + + return Optional.empty(); } } - class Linux extends SimplePathType implements WezTerminalType { + class Linux extends ExternalApplicationType implements WezTerminalType { public Linux() { - super("app.wezterm", "wezterm-gui", true); + super("app.wezterm"); + } + + public boolean isAvailable() { + try (ShellControl pc = LocalShell.getShell()) { + return pc.executeSimpleBooleanCommand(pc.getShellDialect().getWhichCommand("wezterm")) && + pc.executeSimpleBooleanCommand(pc.getShellDialect().getWhichCommand("wezterm-gui")); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).omit().handle(); + return false; + } } @Override - protected CommandBuilder toCommand(LaunchConfiguration configuration) { - return CommandBuilder.of().add("start").addFile(configuration.getScriptFile()); + public void launch(LaunchConfiguration configuration) throws Exception { + var spawn = LocalShell.getShell().command(CommandBuilder.of().addFile("wezterm") + .add("cli", "spawn") + .addFile(configuration.getScriptFile())) + .executeAndCheck(); + if (!spawn) { + ExternalApplicationHelper.startAsync(CommandBuilder.of() + .addFile("wezterm-gui") + .add("start") + .addFile(configuration.getScriptFile())); + } } } @@ -81,20 +123,27 @@ public interface WezTerminalType extends ExternalTerminalType { @Override public void launch(LaunchConfiguration configuration) throws Exception { - var path = LocalShell.getShell() - .command(String.format( - "mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications 2>/dev/null", - applicationName)) - .readStdoutOrThrow(); - var c = CommandBuilder.of() - .addFile(Path.of(path) - .resolve("Contents") - .resolve("MacOS") - .resolve("wezterm-gui") - .toString()) - .add("start") - .add(configuration.getDialectLaunchCommand()); - ExternalApplicationHelper.startAsync(c); + try (var sc = LocalShell.getShell()) { + var path = sc.command( + String.format("mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications 2>/dev/null", + applicationName)).readStdoutOrThrow(); + var spawn = sc.command(CommandBuilder.of().addFile(Path.of(path) + .resolve("Contents") + .resolve("MacOS") + .resolve("wezterm").toString()) + .add("cli", "spawn", "--pane-id", "0") + .addFile(configuration.getScriptFile())) + .executeAndCheck(); + if (!spawn) { + ExternalApplicationHelper.startAsync(CommandBuilder.of() + .addFile(Path.of(path) + .resolve("Contents") + .resolve("MacOS") + .resolve("wezterm-gui").toString()) + .add("start") + .addFile(configuration.getScriptFile())); + } + } } } } diff --git a/app/src/main/java/io/xpipe/app/util/AskpassAlert.java b/app/src/main/java/io/xpipe/app/util/AskpassAlert.java index fee05d49..18243a54 100644 --- a/app/src/main/java/io/xpipe/app/util/AskpassAlert.java +++ b/app/src/main/java/io/xpipe/app/util/AskpassAlert.java @@ -56,6 +56,10 @@ public class AskpassAlert { @Override public void handle(long now) { + if (!stage.isShowing()) { + return; + } + if (regainedFocusCount >= 2) { return; } diff --git a/app/src/main/java/io/xpipe/app/util/CommandView.java b/app/src/main/java/io/xpipe/app/util/CommandView.java index ffcf685a..dce5ee6d 100644 --- a/app/src/main/java/io/xpipe/app/util/CommandView.java +++ b/app/src/main/java/io/xpipe/app/util/CommandView.java @@ -1,13 +1,21 @@ package io.xpipe.app.util; import io.xpipe.core.process.CommandBuilder; +import io.xpipe.core.process.CommandControl; import io.xpipe.core.process.ShellControl; -import lombok.experimental.SuperBuilder; -@SuperBuilder -public abstract class CommandView { +import java.util.function.Consumer; - protected final ShellControl shellControl; +public abstract class CommandView implements AutoCloseable { - protected abstract CommandBuilder base(); + protected abstract CommandControl build(Consumer builder); + + protected abstract ShellControl getShellControl(); + + public abstract CommandView start() throws Exception; + + @Override + public void close() throws Exception { + getShellControl().close(); + } } diff --git a/app/src/main/java/io/xpipe/app/util/CommandViewBase.java b/app/src/main/java/io/xpipe/app/util/CommandViewBase.java new file mode 100644 index 00000000..5a80834f --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/CommandViewBase.java @@ -0,0 +1,18 @@ +package io.xpipe.app.util; + +import io.xpipe.core.process.CommandBuilder; +import io.xpipe.core.process.CommandControl; +import io.xpipe.core.process.ShellControl; +import lombok.Getter; + +import java.util.function.Consumer; + +public abstract class CommandViewBase extends CommandView { + + @Getter + protected final ShellControl shellControl; + + public CommandViewBase(ShellControl shellControl) {this.shellControl = shellControl;} + + protected abstract CommandControl build(Consumer builder); +} diff --git a/app/src/main/java/io/xpipe/app/util/FileOpener.java b/app/src/main/java/io/xpipe/app/util/FileOpener.java index 62390236..bd742a62 100644 --- a/app/src/main/java/io/xpipe/app/util/FileOpener.java +++ b/app/src/main/java/io/xpipe/app/util/FileOpener.java @@ -28,10 +28,9 @@ public class FileOpener { try { editor.launch(Path.of(localFile).toRealPath()); } catch (Exception e) { - ErrorEvent.fromThrowable(e) - .description("Unable to launch editor " + ErrorEvent.fromThrowable("Unable to launch editor " + editor.toTranslatedString().getValue() - + ".\nMaybe try to use a different editor in the settings.") + + ".\nMaybe try to use a different editor in the settings.", e) .expected() .handle(); } @@ -52,8 +51,7 @@ public class FileOpener { } } } catch (Exception e) { - ErrorEvent.fromThrowable(e) - .description("Unable to open file " + localFile) + ErrorEvent.fromThrowable("Unable to open file " + localFile, e) .handle(); } } @@ -68,8 +66,7 @@ public class FileOpener { pc.executeSimpleCommand("open \"" + localFile + "\""); } } catch (Exception e) { - ErrorEvent.fromThrowable(e) - .description("Unable to open file " + localFile) + ErrorEvent.fromThrowable("Unable to open file " + localFile, e) .handle(); } } diff --git a/app/src/main/java/io/xpipe/app/util/HostHelper.java b/app/src/main/java/io/xpipe/app/util/HostHelper.java index c73857f6..9e337ff0 100644 --- a/app/src/main/java/io/xpipe/app/util/HostHelper.java +++ b/app/src/main/java/io/xpipe/app/util/HostHelper.java @@ -6,6 +6,14 @@ import java.util.Locale; public class HostHelper { + private static int portCounter = 0; + + public static int randomPort() { + var p = 40000 + portCounter; + portCounter = portCounter + 1 % 1000; + return p; + } + public static int findRandomOpenPortOnAllLocalInterfaces() throws IOException { try (ServerSocket socket = new ServerSocket(0)) { return socket.getLocalPort(); diff --git a/app/src/main/java/io/xpipe/app/util/LicenseConnectionLimit.java b/app/src/main/java/io/xpipe/app/util/LicenseConnectionLimit.java new file mode 100644 index 00000000..7210023a --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/LicenseConnectionLimit.java @@ -0,0 +1,32 @@ +package io.xpipe.app.util; + +import io.xpipe.app.storage.DataStorage; +import io.xpipe.core.store.DataStore; + +public abstract class LicenseConnectionLimit { + + private final int limit; + private final LicensedFeature feature; + + public LicenseConnectionLimit(int limit, LicensedFeature feature) { + this.limit = limit; + this.feature = feature; + } + + protected abstract boolean matches(DataStore store); + + public void checkLimit() { + if (feature.isSupported()) { + return; + } + + var found = DataStorage.get() + .getStoreEntries() + .stream() + .filter(entry -> entry.getValidity().isUsable() && matches(entry.getStore())) + .toList(); + if (found.size() > limit) { + throw new LicenseRequiredException(feature, limit); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/util/LicenseRequiredException.java b/app/src/main/java/io/xpipe/app/util/LicenseRequiredException.java index 53bae986..2e117309 100644 --- a/app/src/main/java/io/xpipe/app/util/LicenseRequiredException.java +++ b/app/src/main/java/io/xpipe/app/util/LicenseRequiredException.java @@ -1,7 +1,6 @@ package io.xpipe.app.util; import io.xpipe.app.core.AppI18n; - import lombok.Getter; @Getter @@ -15,6 +14,12 @@ public class LicenseRequiredException extends RuntimeException { this.feature = feature; } + public LicenseRequiredException(LicensedFeature feature, int limit) { + super(feature.getDisplayName() + " " + + (feature.isPlural() ? AppI18n.get("areOnlySupportedLimit", limit) : AppI18n.get("isOnlySupportedLimit", limit))); + this.feature = feature; + } + public LicenseRequiredException(String featureName, boolean plural, LicensedFeature feature) { super(featureName + " " + (plural ? AppI18n.get("areOnlySupported") : AppI18n.get("isOnlySupported"))); this.feature = feature; diff --git a/app/src/main/java/io/xpipe/app/util/LocalShell.java b/app/src/main/java/io/xpipe/app/util/LocalShell.java index 18f8f49e..939ceaaa 100644 --- a/app/src/main/java/io/xpipe/app/util/LocalShell.java +++ b/app/src/main/java/io/xpipe/app/util/LocalShell.java @@ -41,6 +41,7 @@ public class LocalShell { localPowershell = ProcessControlProvider.get() .createLocalProcessControl(false) .subShell(ShellDialects.POWERSHELL) + .withoutLicenseCheck() .start(); } return localPowershell.start(); diff --git a/app/src/main/java/io/xpipe/app/util/MarkdownHelper.java b/app/src/main/java/io/xpipe/app/util/MarkdownHelper.java index 63916701..296bb830 100644 --- a/app/src/main/java/io/xpipe/app/util/MarkdownHelper.java +++ b/app/src/main/java/io/xpipe/app/util/MarkdownHelper.java @@ -6,6 +6,8 @@ import com.vladsch.flexmark.ext.footnotes.FootnoteExtension; import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension; import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension; import com.vladsch.flexmark.ext.tables.TablesExtension; +import com.vladsch.flexmark.ext.toc.TocExtension; +import com.vladsch.flexmark.ext.yaml.front.matter.YamlFrontMatterExtension; import com.vladsch.flexmark.html.HtmlRenderer; import com.vladsch.flexmark.parser.Parser; import com.vladsch.flexmark.util.ast.Document; @@ -16,14 +18,16 @@ import java.util.function.UnaryOperator; public class MarkdownHelper { - public static String toHtml(String value, UnaryOperator htmlTransformation) { + public static String toHtml(String value, UnaryOperator headTransformation, UnaryOperator bodyTransformation) { MutableDataSet options = new MutableDataSet().set(Parser.EXTENSIONS, Arrays.asList( StrikethroughExtension.create(), TaskListExtension.create(), TablesExtension.create(), FootnoteExtension.create(), DefinitionExtension.create(), - AnchorLinkExtension.create() + AnchorLinkExtension.create(), + YamlFrontMatterExtension.create(), + TocExtension.create() )) .set(FootnoteExtension.FOOTNOTE_BACK_LINK_REF_CLASS,"footnotes") .set(TablesExtension.WITH_CAPTION, false) @@ -38,7 +42,8 @@ public class MarkdownHelper { HtmlRenderer renderer = HtmlRenderer.builder(options).build(); Document document = parser.parse(value); var html = renderer.render(document); - var result = htmlTransformation.apply(html); - return "
" + result + "
"; + var result = bodyTransformation.apply(html); + var headContent = headTransformation.apply(""); + return "" + headContent + "
" + result + "
"; } } diff --git a/app/src/main/java/io/xpipe/app/util/OptionsBuilder.java b/app/src/main/java/io/xpipe/app/util/OptionsBuilder.java index ad032cae..5725b5a5 100644 --- a/app/src/main/java/io/xpipe/app/util/OptionsBuilder.java +++ b/app/src/main/java/io/xpipe/app/util/OptionsBuilder.java @@ -23,6 +23,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; public class OptionsBuilder { @@ -146,6 +147,11 @@ public class OptionsBuilder { return this; } + public OptionsBuilder check(Function c) { + lastCompHeadReference.apply(s -> c.apply(ownValidator).decorates(s.get())); + return this; + } + public OptionsBuilder check(Check c) { lastCompHeadReference.apply(s -> c.decorates(s.get())); return this; @@ -221,7 +227,7 @@ public class OptionsBuilder { } public OptionsBuilder addToggle(Property prop) { - var comp = new ToggleSwitchComp(prop, null); + var comp = new ToggleSwitchComp(prop, null, null); pushComp(comp); props.add(prop); return this; diff --git a/app/src/main/java/io/xpipe/app/util/TerminalLauncherManager.java b/app/src/main/java/io/xpipe/app/util/TerminalLauncherManager.java index 936e34e4..152c02bf 100644 --- a/app/src/main/java/io/xpipe/app/util/TerminalLauncherManager.java +++ b/app/src/main/java/io/xpipe/app/util/TerminalLauncherManager.java @@ -1,13 +1,12 @@ package io.xpipe.app.util; -import io.xpipe.beacon.ClientException; -import io.xpipe.beacon.ServerException; +import io.xpipe.beacon.BeaconClientException; +import io.xpipe.beacon.BeaconServerException; import io.xpipe.core.process.ProcessControl; import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.TerminalInitScriptConfig; import io.xpipe.core.process.WorkingDirectoryFunction; import io.xpipe.core.store.FilePath; - import lombok.Setter; import lombok.Value; import lombok.experimental.NonFinal; @@ -73,10 +72,10 @@ public class TerminalLauncherManager { return latch; } - public static Path waitForCompletion(UUID request) throws ClientException, ServerException { + public static Path waitForCompletion(UUID request) throws BeaconClientException, BeaconServerException { var e = entries.get(request); if (e == null) { - throw new ClientException("Unknown launch request " + request); + throw new BeaconClientException("Unknown launch request " + request); } while (true) { @@ -89,21 +88,21 @@ public class TerminalLauncherManager { if (r instanceof ResultFailure failure) { entries.remove(request); var t = failure.getThrowable(); - throw new ServerException(t); + throw new BeaconServerException(t); } return ((ResultSuccess) r).getTargetScript(); } } - public static Path performLaunch(UUID request) throws ClientException { + public static Path performLaunch(UUID request) throws BeaconClientException { var e = entries.remove(request); if (e == null) { - throw new ClientException("Unknown launch request " + request); + throw new BeaconClientException("Unknown launch request " + request); } if (!(e.result instanceof ResultSuccess)) { - throw new ClientException("Invalid launch request state " + request); + throw new BeaconClientException("Invalid launch request state " + request); } return ((ResultSuccess) e.getResult()).getTargetScript(); diff --git a/app/src/main/java/module-info.java b/app/src/main/java/module-info.java index a0d3e828..01b8e79c 100644 --- a/app/src/main/java/module-info.java +++ b/app/src/main/java/module-info.java @@ -1,7 +1,7 @@ +import com.fasterxml.jackson.databind.Module; +import io.xpipe.app.beacon.impl.*; import io.xpipe.app.browser.action.BrowserAction; import io.xpipe.app.core.AppLogs; -import io.xpipe.app.exchange.*; -import io.xpipe.app.exchange.cli.*; import io.xpipe.app.ext.*; import io.xpipe.app.issue.EventHandler; import io.xpipe.app.issue.EventHandlerImpl; @@ -10,15 +10,15 @@ import io.xpipe.app.util.AppJacksonModule; import io.xpipe.app.util.LicenseProvider; import io.xpipe.app.util.ProxyManagerProviderImpl; import io.xpipe.app.util.TerminalLauncher; +import io.xpipe.beacon.BeaconInterface; import io.xpipe.core.util.DataStateProvider; import io.xpipe.core.util.ModuleLayerLoader; import io.xpipe.core.util.ProxyFunction; import io.xpipe.core.util.ProxyManagerProvider; - -import com.fasterxml.jackson.databind.Module; import org.slf4j.spi.SLF4JServiceProvider; open module io.xpipe.app { + exports io.xpipe.app.beacon; exports io.xpipe.app.core; exports io.xpipe.app.util; exports io.xpipe.app; @@ -52,6 +52,7 @@ open module io.xpipe.app { requires com.vladsch.flexmark; requires com.fasterxml.jackson.core; requires com.fasterxml.jackson.databind; + requires com.fasterxml.jackson.annotation; requires net.synedra.validatorfx; requires org.kordamp.ikonli.feather; requires io.xpipe.modulefs; @@ -79,14 +80,6 @@ open module io.xpipe.app { requires net.steppschuh.markdowngenerator; requires com.shinyhut.vernacular; - // Required by extensions - requires java.security.jgss; - requires java.security.sasl; - requires java.xml; - requires java.xml.crypto; - requires java.sql; - requires java.sql.rowset; - // Required runtime modules requires jdk.charsets; requires jdk.crypto.cryptoki; @@ -100,8 +93,8 @@ open module io.xpipe.app { // For debugging requires jdk.jdwp.agent; requires org.kordamp.ikonli.core; + requires jdk.httpserver; - uses MessageExchangeImpl; uses TerminalLauncher; uses io.xpipe.app.ext.ActionProvider; uses EventHandler; @@ -113,11 +106,13 @@ open module io.xpipe.app { uses BrowserAction; uses LicenseProvider; uses io.xpipe.app.util.LicensedFeature; + uses io.xpipe.beacon.BeaconInterface; + uses DataStorageExtensionProvider; provides Module with AppJacksonModule; provides ModuleLayerLoader with - MessageExchangeImpls.Loader, + DataStorageExtensionProvider.Loader, DataStoreProviders.Loader, ActionProvider.Loader, PrefsProvider.Loader, @@ -132,26 +127,9 @@ open module io.xpipe.app { AppLogs.Slf4jProvider; provides EventHandler with EventHandlerImpl; - provides MessageExchangeImpl with - ReadDrainExchangeImpl, - EditStoreExchangeImpl, - StoreProviderListExchangeImpl, - OpenExchangeImpl, - LaunchExchangeImpl, - FocusExchangeImpl, - StatusExchangeImpl, - DrainExchangeImpl, - SinkExchangeImpl, - StopExchangeImpl, - ModeExchangeImpl, - DialogExchangeImpl, - RemoveStoreExchangeImpl, - RenameStoreExchangeImpl, - ListStoresExchangeImpl, - StoreAddExchangeImpl, + provides BeaconInterface with ShellStartExchangeImpl, ShellStopExchangeImpl, ShellExecExchangeImpl, ConnectionQueryExchangeImpl, DaemonOpenExchangeImpl, DaemonFocusExchangeImpl, DaemonStatusExchangeImpl, DaemonStopExchangeImpl, + HandshakeExchangeImpl, DaemonModeExchangeImpl, AskpassExchangeImpl, TerminalWaitExchangeImpl, - TerminalLaunchExchangeImpl, - QueryStoreExchangeImpl, - VersionExchangeImpl; + TerminalLaunchExchangeImpl, DaemonVersionExchangeImpl; } 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 new file mode 100644 index 00000000..8a4b6301 --- /dev/null +++ b/app/src/main/resources/io/xpipe/app/resources/misc/api.md @@ -0,0 +1,1249 @@ +--- +title: XPipe API Documentation v10.0 +language_tabs: + - javascript: JavaScript + - python: Python + - java: Java + - go: Go + - shell: Shell +language_clients: + - javascript: "" + - python: "" + - java: "" + - go: "" + - shell: "" +toc_footers: + - XPipe - Plans and pricing +includes: [] +search: true +highlight_theme: darkula +headingLevel: 2 + +--- + +

XPipe API Documentation v10.0

+ +The XPipe API provides programmatic access to XPipe’s features. + +The XPipe application will start up an HTTP server that can be used to send requests. +You can change the port of it in the settings menu. +Note that this server is HTTP-only for now as it runs only on localhost. HTTPS requests are not accepted. + +This allows you to programmatically manage remote systems. +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. +If a required password is not stored and is set to be dynamically prompted, the running XPipe application will ask you to enter any required passwords. + +You can quickly get started by either using this page as an API reference or alternatively import the [OpenAPI definition file](/openapi.yaml) into your API client of choice. +See the authentication handshake below on how to authenticate prior to sending requests. + +Base URLs: + +* http://localhost:21721 + +Table of contents: +[TOC] + +# Authentication + +- HTTP Authentication, scheme: bearer The bearer token used is the session token that you receive from the handshake exchange. + +

Default

+ +## Establish a new API session + + + +`POST /handshake` + +Prior to sending requests to the API, you first have to establish a new API session via the handshake endpoint. +In the response you will receive a session token that you can use to authenticate during this session. + +This is done so that the daemon knows what kind of clients are connected and can manage individual capabilities for clients. + +Note that for development you can also turn off the required authentication in the XPipe settings menu, allowing you to send unauthenticated requests. + +> Body parameter + +```json +{ + "auth": { + "type": "ApiKey", + "key": "" + }, + "client": { + "type": "Api", + "name": "My client name" + } +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[HandshakeRequest](#schemahandshakerequest)|true|none| + +> Example responses + +> 200 Response + +```json +{ + "sessionToken": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|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.|[HandshakeResponse](#schemahandshakeresponse)| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal error.|None| + + + +
+ +Code samples + +```javascript +const inputBody = '{ + "auth": { + "type": "ApiKey", + "key": "" + }, + "client": { + "type": "Api", + "name": "My client name" + } +}'; +const headers = { + 'Content-Type':'application/json', + 'Accept':'application/json' +}; + +fetch('http://localhost:21721/handshake', +{ + method: 'POST', + body: inputBody, + headers: headers +}) +.then(function(res) { + return res.json(); +}).then(function(body) { + console.log(body); +}); + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json' +} + +data = """ +{ + "auth": { + "type": "ApiKey", + "key": "" + }, + "client": { + "type": "Api", + "name": "My client name" + } +} +""" +r = requests.post('http://localhost:21721/handshake', headers = headers, data = data) + +print(r.json()) + +``` + +```java +var uri = URI.create("http://localhost:21721/handshake"); +var client = HttpClient.newHttpClient(); +var request = HttpRequest + .newBuilder() + .uri(uri) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(""" +{ + "auth": { + "type": "ApiKey", + "key": "" + }, + "client": { + "type": "Api", + "name": "My client name" + } +} + """)) + .build(); +var response = client.send(request, HttpResponse.BodyHandlers.ofString()); +System.out.println(response.statusCode()); +System.out.println(response.body()); + +``` + +```go +package main + +import ( + "bytes" + "net/http" +) + +func main() { + + headers := map[string][]string{ + "Content-Type": []string{"application/json"}, + "Accept": []string{"application/json"}, + } + + data := bytes.NewBuffer([]byte{jsonReq}) + req, err := http.NewRequest("POST", "http://localhost:21721/handshake", data) + req.Header = headers + + client := &http.Client{} + resp, err := client.Do(req) + // ... +} + +``` + +```shell +# You can also use wget +curl -X POST http://localhost:21721/handshake \ + -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ + --data ' +{ + "auth": { + "type": "ApiKey", + "key": "" + }, + "client": { + "type": "Api", + "name": "My client name" + } +} +' + +``` + +
+ +## Query connections + + + +`POST /connection/query` + +Queries all connections using various filters. + +The filters support globs and can match the category names and connection names. +All matching is case insensitive. + +> Body parameter + +```json +{ + "categoryFilter": "*", + "connectionFilter": "*", + "typeFilter": "*" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[ConnectionQueryRequest](#schemaconnectionqueryrequest)|true|none| + +> Example responses + +> The query was successful. The body contains all matched connections. + +```json +{ + "found": [ + { + "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b", + "category": [ + "default" + ], + "connection": [ + "local machine" + ], + "type": "local" + }, + { + "uuid": "e1462ddc-9beb-484c-bd91-bb666027e300", + "category": [ + "default", + "category 1" + ], + "connection": [ + "ssh system", + "shell environments", + "bash" + ], + "type": "shellEnvironment" + } + ] +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|The query was successful. The body contains all matched connections.|[ConnectionQueryResponse](#schemaconnectionqueryresponse)| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Authorization failed. Please supply a `Bearer` token via the `Authorization` header.|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Authorization failed. Please supply a valid `Bearer` token via the `Authorization` header.|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|The requested resource could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal error.|None| + + + +
+ +Code samples + +```javascript +const inputBody = '{ + "categoryFilter": "*", + "connectionFilter": "*", + "typeFilter": "*" +}'; +const headers = { + 'Content-Type':'application/json', + 'Accept':'application/json', + 'Authorization':'Bearer {access-token}' +}; + +fetch('http://localhost:21721/connection/query', +{ + method: 'POST', + body: inputBody, + headers: headers +}) +.then(function(res) { + return res.json(); +}).then(function(body) { + console.log(body); +}); + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +data = """ +{ + "categoryFilter": "*", + "connectionFilter": "*", + "typeFilter": "*" +} +""" +r = requests.post('http://localhost:21721/connection/query', headers = headers, data = data) + +print(r.json()) + +``` + +```java +var uri = URI.create("http://localhost:21721/connection/query"); +var client = HttpClient.newHttpClient(); +var request = HttpRequest + .newBuilder() + .uri(uri) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .header("Authorization", "Bearer {access-token}") + .POST(HttpRequest.BodyPublishers.ofString(""" +{ + "categoryFilter": "*", + "connectionFilter": "*", + "typeFilter": "*" +} + """)) + .build(); +var response = client.send(request, HttpResponse.BodyHandlers.ofString()); +System.out.println(response.statusCode()); +System.out.println(response.body()); + +``` + +```go +package main + +import ( + "bytes" + "net/http" +) + +func main() { + + headers := map[string][]string{ + "Content-Type": []string{"application/json"}, + "Accept": []string{"application/json"}, + "Authorization": []string{"Bearer {access-token}"}, + } + + data := bytes.NewBuffer([]byte{jsonReq}) + req, err := http.NewRequest("POST", "http://localhost:21721/connection/query", data) + req.Header = headers + + client := &http.Client{} + resp, err := client.Do(req) + // ... +} + +``` + +```shell +# You can also use wget +curl -X POST http://localhost:21721/connection/query \ + -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer {access-token}' \ + --data ' +{ + "categoryFilter": "*", + "connectionFilter": "*", + "typeFilter": "*" +} +' + +``` + +
+ +## Start shell connection + + + +`POST /shell/start` + +Starts a new shell session for a connection. If an existing shell session is already running for that connection, this operation will do nothing. + +Note that there are a variety of possible errors that can occur here when establishing the shell connection. +These errors will be returned with the HTTP return code 500. + +> Body parameter + +```json +{ + "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[ShellStartRequest](#schemashellstartrequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|The operation was successful. The shell session was started.|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Authorization failed. Please supply a `Bearer` token via the `Authorization` header.|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Authorization failed. Please supply a valid `Bearer` token via the `Authorization` header.|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|The requested resource could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal error.|None| + + + +
+ +Code samples + +```javascript +const inputBody = '{ + "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b" +}'; +const headers = { + 'Content-Type':'application/json', + 'Authorization':'Bearer {access-token}' +}; + +fetch('http://localhost:21721/shell/start', +{ + method: 'POST', + body: inputBody, + headers: headers +}) +.then(function(res) { + return res.json(); +}).then(function(body) { + console.log(body); +}); + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +data = """ +{ + "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b" +} +""" +r = requests.post('http://localhost:21721/shell/start', headers = headers, data = data) + +print(r.json()) + +``` + +```java +var uri = URI.create("http://localhost:21721/shell/start"); +var client = HttpClient.newHttpClient(); +var request = HttpRequest + .newBuilder() + .uri(uri) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer {access-token}") + .POST(HttpRequest.BodyPublishers.ofString(""" +{ + "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b" +} + """)) + .build(); +var response = client.send(request, HttpResponse.BodyHandlers.ofString()); +System.out.println(response.statusCode()); +System.out.println(response.body()); + +``` + +```go +package main + +import ( + "bytes" + "net/http" +) + +func main() { + + headers := map[string][]string{ + "Content-Type": []string{"application/json"}, + "Authorization": []string{"Bearer {access-token}"}, + } + + data := bytes.NewBuffer([]byte{jsonReq}) + req, err := http.NewRequest("POST", "http://localhost:21721/shell/start", data) + req.Header = headers + + client := &http.Client{} + resp, err := client.Do(req) + // ... +} + +``` + +```shell +# You can also use wget +curl -X POST http://localhost:21721/shell/start \ + -H 'Content-Type: application/json' \ -H 'Authorization: Bearer {access-token}' \ + --data ' +{ + "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b" +} +' + +``` + +
+ +## Stop shell connection + + + +`POST /shell/stop` + +Stops an existing shell session for a connection. + +This operation will return once the shell has exited. +If the shell is busy or stuck, you might have to work with timeouts to account for these cases. + +> Body parameter + +```json +{ + "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[ShellStopRequest](#schemashellstoprequest)|true|none| + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|The operation was successful. The shell session was stopped.|None| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Authorization failed. Please supply a `Bearer` token via the `Authorization` header.|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Authorization failed. Please supply a valid `Bearer` token via the `Authorization` header.|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|The requested resource could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal error.|None| + + + +
+ +Code samples + +```javascript +const inputBody = '{ + "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b" +}'; +const headers = { + 'Content-Type':'application/json', + 'Authorization':'Bearer {access-token}' +}; + +fetch('http://localhost:21721/shell/stop', +{ + method: 'POST', + body: inputBody, + headers: headers +}) +.then(function(res) { + return res.json(); +}).then(function(body) { + console.log(body); +}); + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +data = """ +{ + "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b" +} +""" +r = requests.post('http://localhost:21721/shell/stop', headers = headers, data = data) + +print(r.json()) + +``` + +```java +var uri = URI.create("http://localhost:21721/shell/stop"); +var client = HttpClient.newHttpClient(); +var request = HttpRequest + .newBuilder() + .uri(uri) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer {access-token}") + .POST(HttpRequest.BodyPublishers.ofString(""" +{ + "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b" +} + """)) + .build(); +var response = client.send(request, HttpResponse.BodyHandlers.ofString()); +System.out.println(response.statusCode()); +System.out.println(response.body()); + +``` + +```go +package main + +import ( + "bytes" + "net/http" +) + +func main() { + + headers := map[string][]string{ + "Content-Type": []string{"application/json"}, + "Authorization": []string{"Bearer {access-token}"}, + } + + data := bytes.NewBuffer([]byte{jsonReq}) + req, err := http.NewRequest("POST", "http://localhost:21721/shell/stop", data) + req.Header = headers + + client := &http.Client{} + resp, err := client.Do(req) + // ... +} + +``` + +```shell +# You can also use wget +curl -X POST http://localhost:21721/shell/stop \ + -H 'Content-Type: application/json' \ -H 'Authorization: Bearer {access-token}' \ + --data ' +{ + "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b" +} +' + +``` + +
+ +## Execute command in a shell session + + + +`POST /shell/exec` + +Runs a command in an active shell session and waits for it to finish. The exit code and output will be returned in the response. + +Note that a variety of different errors can occur when executing the command. +If the command finishes, even with an error code, a normal HTTP 200 response will be returned. +However, if any other error occurs like the shell not responding or exiting unexpectedly, an HTTP 500 response will be returned. + +> Body parameter + +```json +{ + "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b", + "command": "echo $USER" +} +``` + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|body|body|[ShellExecRequest](#schemashellexecrequest)|true|none| + +> Example responses + +> The operation was successful. The shell command finished. + +```json +{ + "exitCode": 0, + "stdout": "root", + "stderr": "" +} +``` + +```json +{ + "exitCode": 127, + "stdout": "", + "stderr": "invalid: command not found" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|The operation was successful. The shell command finished.|[ShellExecResponse](#schemashellexecresponse)| +|400|[Bad Request](https://tools.ietf.org/html/rfc7231#section-6.5.1)|Bad request. Please check error message and your parameters.|None| +|401|[Unauthorized](https://tools.ietf.org/html/rfc7235#section-3.1)|Authorization failed. Please supply a `Bearer` token via the `Authorization` header.|None| +|403|[Forbidden](https://tools.ietf.org/html/rfc7231#section-6.5.3)|Authorization failed. Please supply a valid `Bearer` token via the `Authorization` header.|None| +|404|[Not Found](https://tools.ietf.org/html/rfc7231#section-6.5.4)|The requested resource could not be found.|None| +|500|[Internal Server Error](https://tools.ietf.org/html/rfc7231#section-6.6.1)|Internal error.|None| + + + +
+ +Code samples + +```javascript +const inputBody = '{ + "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b", + "command": "echo $USER" +}'; +const headers = { + 'Content-Type':'application/json', + 'Accept':'application/json', + 'Authorization':'Bearer {access-token}' +}; + +fetch('http://localhost:21721/shell/exec', +{ + method: 'POST', + body: inputBody, + headers: headers +}) +.then(function(res) { + return res.json(); +}).then(function(body) { + console.log(body); +}); + +``` + +```python +import requests +headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': 'Bearer {access-token}' +} + +data = """ +{ + "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b", + "command": "echo $USER" +} +""" +r = requests.post('http://localhost:21721/shell/exec', headers = headers, data = data) + +print(r.json()) + +``` + +```java +var uri = URI.create("http://localhost:21721/shell/exec"); +var client = HttpClient.newHttpClient(); +var request = HttpRequest + .newBuilder() + .uri(uri) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .header("Authorization", "Bearer {access-token}") + .POST(HttpRequest.BodyPublishers.ofString(""" +{ + "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b", + "command": "echo $USER" +} + """)) + .build(); +var response = client.send(request, HttpResponse.BodyHandlers.ofString()); +System.out.println(response.statusCode()); +System.out.println(response.body()); + +``` + +```go +package main + +import ( + "bytes" + "net/http" +) + +func main() { + + headers := map[string][]string{ + "Content-Type": []string{"application/json"}, + "Accept": []string{"application/json"}, + "Authorization": []string{"Bearer {access-token}"}, + } + + data := bytes.NewBuffer([]byte{jsonReq}) + req, err := http.NewRequest("POST", "http://localhost:21721/shell/exec", data) + req.Header = headers + + client := &http.Client{} + resp, err := client.Do(req) + // ... +} + +``` + +```shell +# You can also use wget +curl -X POST http://localhost:21721/shell/exec \ + -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer {access-token}' \ + --data ' +{ + "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b", + "command": "echo $USER" +} +' + +``` + +
+ +# Schemas + +

ShellStartRequest

+ + + + + + +```json +{ + "connection": "string" +} + +``` + +

Properties

+ +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|connection|string|true|none|The connection uuid| + +

ShellStopRequest

+ + + + + + +```json +{ + "connection": "string" +} + +``` + +

Properties

+ +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|connection|string|true|none|The connection uuid| + +

ShellExecRequest

+ + + + + + +```json +{ + "connection": "string", + "command": "string" +} + +``` + +

Properties

+ +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|connection|string|true|none|The connection uuid| +|command|string|true|none|The command to execute| + +

ShellExecResponse

+ + + + + + +```json +{ + "exitCode": 0, + "stdout": "string", + "stderr": "string" +} + +``` + +

Properties

+ +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|exitCode|integer|true|none|The exit code of the command| +|stdout|string|true|none|The stdout output of the command| +|stderr|string|true|none|The stderr output of the command| + +

ConnectionQueryRequest

+ + + + + + +```json +{ + "categoryFilter": "string", + "connectionFilter": "string", + "typeFilter": "string" +} + +``` + +

Properties

+ +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|categoryFilter|string|true|none|The filter string to match categories. Categories are delimited by / if they are hierarchical. The filter supports globs.| +|connectionFilter|string|true|none|The filter string to match connection names. Connection names are delimited by / if they are hierarchical. The filter supports globs.| +|typeFilter|string|true|none|The filter string to connection types. Every unique type of connection like SSH or docker has its own type identifier that you can match. The filter supports globs.| + +

ConnectionQueryResponse

+ + + + + + +```json +{ + "found": [ + { + "uuid": "string", + "category": [ + "string" + ], + "connection": [ + "string" + ], + "type": "string" + } + ] +} + +``` + +

Properties

+ +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|found|[object]|true|none|The found connections| +|» uuid|string|true|none|The unique id of the connection| +|» category|[string]|true|none|The full category path as an array| +|» connection|[string]|true|none|The full connection name path as an array| +|» type|string|true|none|The type identifier of the connection| + +

HandshakeRequest

+ + + + + + +```json +{ + "auth": { + "type": "string", + "key": "string" + }, + "client": { + "type": "string" + } +} + +``` + +

Properties

+ +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|auth|[AuthMethod](#schemaauthmethod)|true|none|none| +|client|[ClientInformation](#schemaclientinformation)|true|none|none| + +

HandshakeResponse

+ + + + + + +```json +{ + "sessionToken": "string" +} + +``` + +

Properties

+ +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|sessionToken|string|true|none|The generated bearer token that can be used for authentication in this session| + +

AuthMethod

+ + + + + + +```json +{ + "type": "string", + "key": "string" +} + +``` + +

Properties

+ +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|type|string|true|none|none| + +oneOf + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[ApiKey](#schemaapikey)|false|none|API key authentication| + +xor + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[Local](#schemalocal)|false|none|Authentication method for local applications. Uses file system access as proof of authentication.| + +

ApiKey

+ + + + + + +```json +{ + "type": "string", + "key": "string" +} + +``` + +API key authentication + +

Properties

+ +allOf - discriminator: AuthMethod.type + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[AuthMethod](#schemaauthmethod)|false|none|none| + +and + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|object|false|none|none| +|» key|string|true|none|The API key| + +

Local

+ + + + + + +```json +{ + "type": "string", + "key": "string", + "authFileContent": "string" +} + +``` + +Authentication method for local applications. Uses file system access as proof of authentication. + +

Properties

+ +allOf - discriminator: AuthMethod.type + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[AuthMethod](#schemaauthmethod)|false|none|none| + +and + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|object|false|none|none| +|» authFileContent|string|true|none|The contents of the local file $TEMP/xpipe_auth. This file is automatically generated when XPipe starts.| + +

ClientInformation

+ + + + + + +```json +{ + "type": "string" +} + +``` + +

Properties

+ +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|type|string|true|none|none| + +

ApiClientInformation

+ + + + + + +```json +{ + "type": "string", + "name": "string" +} + +``` + +Provides information about the client that connected to the XPipe API. + +

Properties

+ +allOf - discriminator: ClientInformation.type + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[ClientInformation](#schemaclientinformation)|false|none|none| + +and + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|object|false|none|none| +|» name|string|true|none|The name of the client.| + diff --git a/app/src/main/resources/io/xpipe/app/resources/misc/github-dark.min.css b/app/src/main/resources/io/xpipe/app/resources/misc/github-dark.min.css new file mode 100644 index 00000000..03b6da8b --- /dev/null +++ b/app/src/main/resources/io/xpipe/app/resources/misc/github-dark.min.css @@ -0,0 +1,10 @@ +pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*! + Theme: GitHub Dark + Description: Dark theme as seen on github.com + Author: github.com + Maintainer: @Hirse + Updated: 2021-05-15 + + Outdated base version: https://github.com/primer/github-syntax-dark + Current colors taken from GitHub's CSS +*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c} \ No newline at end of file diff --git a/app/src/main/resources/io/xpipe/app/resources/web/github-markdown-dark.css b/app/src/main/resources/io/xpipe/app/resources/misc/github-markdown-dark.css similarity index 99% rename from app/src/main/resources/io/xpipe/app/resources/web/github-markdown-dark.css rename to app/src/main/resources/io/xpipe/app/resources/misc/github-markdown-dark.css index b818a8b7..4fe44189 100644 --- a/app/src/main/resources/io/xpipe/app/resources/web/github-markdown-dark.css +++ b/app/src/main/resources/io/xpipe/app/resources/misc/github-markdown-dark.css @@ -70,6 +70,12 @@ html { font-style: italic; } +.markdown-body summary { + font-weight: 600; + padding-bottom: .3em; + font-size: 1.4em; +} + .markdown-body h1 { margin: .67em 0; font-weight: 600; @@ -416,7 +422,7 @@ html { .markdown-body table, .markdown-body pre, .markdown-body details { - margin-top: 0; + margin-top: 16px; margin-bottom: 16px; } diff --git a/app/src/main/resources/io/xpipe/app/resources/web/github-markdown-light.css b/app/src/main/resources/io/xpipe/app/resources/misc/github-markdown-light.css similarity index 99% rename from app/src/main/resources/io/xpipe/app/resources/web/github-markdown-light.css rename to app/src/main/resources/io/xpipe/app/resources/misc/github-markdown-light.css index 1c792edb..c69cdde9 100644 --- a/app/src/main/resources/io/xpipe/app/resources/web/github-markdown-light.css +++ b/app/src/main/resources/io/xpipe/app/resources/misc/github-markdown-light.css @@ -542,10 +542,16 @@ html { .markdown-body table, .markdown-body pre, .markdown-body details { - margin-top: 0; + margin-top: 16px; margin-bottom: 16px; } +.markdown-body summary { + font-weight: 600; + padding-bottom: .3em; + font-size: 1.4em; +} + .markdown-body blockquote > :first-child { margin-top: 0; } diff --git a/app/src/main/resources/io/xpipe/app/resources/misc/highlight.min.js b/app/src/main/resources/io/xpipe/app/resources/misc/highlight.min.js new file mode 100644 index 00000000..c5c1ca50 --- /dev/null +++ b/app/src/main/resources/io/xpipe/app/resources/misc/highlight.min.js @@ -0,0 +1,1381 @@ +/*! + Highlight.js v11.9.0 (git: b7ec4bfafc) + (c) 2006-2024 undefined and other contributors + License: BSD-3-Clause + */ +var hljs=function(){"use strict";function e(t){ +return t instanceof Map?t.clear=t.delete=t.set=()=>{ +throw Error("map is read-only")}:t instanceof Set&&(t.add=t.clear=t.delete=()=>{ +throw Error("set is read-only") +}),Object.freeze(t),Object.getOwnPropertyNames(t).forEach((n=>{ +const i=t[n],s=typeof i;"object"!==s&&"function"!==s||Object.isFrozen(i)||e(i) +})),t}class t{constructor(e){ +void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1} +ignoreMatch(){this.isMatchIgnored=!0}}function n(e){ +return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'") +}function i(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t] +;return t.forEach((e=>{for(const t in e)n[t]=e[t]})),n}const s=e=>!!e.scope +;class o{constructor(e,t){ +this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){ +this.buffer+=n(e)}openNode(e){if(!s(e))return;const t=((e,{prefix:t})=>{ +if(e.startsWith("language:"))return e.replace("language:","language-") +;if(e.includes(".")){const n=e.split(".") +;return[`${t}${n.shift()}`,...n.map(((e,t)=>`${e}${"_".repeat(t+1)}`))].join(" ") +}return`${t}${e}`})(e.scope,{prefix:this.classPrefix});this.span(t)} +closeNode(e){s(e)&&(this.buffer+="")}value(){return this.buffer}span(e){ +this.buffer+=``}}const r=(e={})=>{const t={children:[]} +;return Object.assign(t,e),t};class a{constructor(){ +this.rootNode=r(),this.stack=[this.rootNode]}get top(){ +return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){ +this.top.children.push(e)}openNode(e){const t=r({scope:e}) +;this.add(t),this.stack.push(t)}closeNode(){ +if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){ +for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)} +walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){ +return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t), +t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){ +"string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{ +a._collapse(e)})))}}class c extends a{constructor(e){super(),this.options=e} +addText(e){""!==e&&this.add(e)}startScope(e){this.openNode(e)}endScope(){ +this.closeNode()}__addSublanguage(e,t){const n=e.root +;t&&(n.scope="language:"+t),this.add(n)}toHTML(){ +return new o(this,this.options).value()}finalize(){ +return this.closeAllNodes(),!0}}function l(e){ +return e?"string"==typeof e?e:e.source:null}function g(e){return h("(?=",e,")")} +function u(e){return h("(?:",e,")*")}function d(e){return h("(?:",e,")?")} +function h(...e){return e.map((e=>l(e))).join("")}function f(...e){const t=(e=>{ +const t=e[e.length-1] +;return"object"==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{} +})(e);return"("+(t.capture?"":"?:")+e.map((e=>l(e))).join("|")+")"} +function p(e){return RegExp(e.toString()+"|").exec("").length-1} +const b=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./ +;function m(e,{joinWith:t}){let n=0;return e.map((e=>{n+=1;const t=n +;let i=l(e),s="";for(;i.length>0;){const e=b.exec(i);if(!e){s+=i;break} +s+=i.substring(0,e.index), +i=i.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?s+="\\"+(Number(e[1])+t):(s+=e[0], +"("===e[0]&&n++)}return s})).map((e=>`(${e})`)).join(t)} +const E="[a-zA-Z]\\w*",x="[a-zA-Z_]\\w*",w="\\b\\d+(\\.\\d+)?",y="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",_="\\b(0b[01]+)",O={ +begin:"\\\\[\\s\\S]",relevance:0},v={scope:"string",begin:"'",end:"'", +illegal:"\\n",contains:[O]},k={scope:"string",begin:'"',end:'"',illegal:"\\n", +contains:[O]},N=(e,t,n={})=>{const s=i({scope:"comment",begin:e,end:t, +contains:[]},n);s.contains.push({scope:"doctag", +begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)", +end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0}) +;const o=f("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/) +;return s.contains.push({begin:h(/[ ]+/,"(",o,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),s +},S=N("//","$"),M=N("/\\*","\\*/"),R=N("#","$");var j=Object.freeze({ +__proto__:null,APOS_STRING_MODE:v,BACKSLASH_ESCAPE:O,BINARY_NUMBER_MODE:{ +scope:"number",begin:_,relevance:0},BINARY_NUMBER_RE:_,COMMENT:N, +C_BLOCK_COMMENT_MODE:M,C_LINE_COMMENT_MODE:S,C_NUMBER_MODE:{scope:"number", +begin:y,relevance:0},C_NUMBER_RE:y,END_SAME_AS_BEGIN:e=>Object.assign(e,{ +"on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{ +t.data._beginMatch!==e[1]&&t.ignoreMatch()}}),HASH_COMMENT_MODE:R,IDENT_RE:E, +MATCH_NOTHING_RE:/\b\B/,METHOD_GUARD:{begin:"\\.\\s*"+x,relevance:0}, +NUMBER_MODE:{scope:"number",begin:w,relevance:0},NUMBER_RE:w, +PHRASAL_WORDS_MODE:{ +begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ +},QUOTE_STRING_MODE:k,REGEXP_MODE:{scope:"regexp",begin:/\/(?=[^/\n]*\/)/, +end:/\/[gimuy]*/,contains:[O,{begin:/\[/,end:/\]/,relevance:0,contains:[O]}]}, +RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~", +SHEBANG:(e={})=>{const t=/^#![ ]*\// +;return e.binary&&(e.begin=h(t,/.*\b/,e.binary,/\b.*/)),i({scope:"meta",begin:t, +end:/$/,relevance:0,"on:begin":(e,t)=>{0!==e.index&&t.ignoreMatch()}},e)}, +TITLE_MODE:{scope:"title",begin:E,relevance:0},UNDERSCORE_IDENT_RE:x, +UNDERSCORE_TITLE_MODE:{scope:"title",begin:x,relevance:0}});function A(e,t){ +"."===e.input[e.index-1]&&t.ignoreMatch()}function I(e,t){ +void 0!==e.className&&(e.scope=e.className,delete e.className)}function T(e,t){ +t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)", +e.__beforeBegin=A,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords, +void 0===e.relevance&&(e.relevance=0))}function L(e,t){ +Array.isArray(e.illegal)&&(e.illegal=f(...e.illegal))}function B(e,t){ +if(e.match){ +if(e.begin||e.end)throw Error("begin & end are not supported with match") +;e.begin=e.match,delete e.match}}function P(e,t){ +void 0===e.relevance&&(e.relevance=1)}const D=(e,t)=>{if(!e.beforeMatch)return +;if(e.starts)throw Error("beforeMatch cannot be used with starts") +;const n=Object.assign({},e);Object.keys(e).forEach((t=>{delete e[t] +})),e.keywords=n.keywords,e.begin=h(n.beforeMatch,g(n.begin)),e.starts={ +relevance:0,contains:[Object.assign(n,{endsParent:!0})] +},e.relevance=0,delete n.beforeMatch +},H=["of","and","for","in","not","or","if","then","parent","list","value"],C="keyword" +;function $(e,t,n=C){const i=Object.create(null) +;return"string"==typeof e?s(n,e.split(" ")):Array.isArray(e)?s(n,e):Object.keys(e).forEach((n=>{ +Object.assign(i,$(e[n],t,n))})),i;function s(e,n){ +t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((t=>{const n=t.split("|") +;i[n[0]]=[e,U(n[0],n[1])]}))}}function U(e,t){ +return t?Number(t):(e=>H.includes(e.toLowerCase()))(e)?0:1}const z={},W=e=>{ +console.error(e)},X=(e,...t)=>{console.log("WARN: "+e,...t)},G=(e,t)=>{ +z[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),z[`${e}/${t}`]=!0) +},K=Error();function F(e,t,{key:n}){let i=0;const s=e[n],o={},r={} +;for(let e=1;e<=t.length;e++)r[e+i]=s[e],o[e+i]=!0,i+=p(t[e-1]) +;e[n]=r,e[n]._emit=o,e[n]._multi=!0}function Z(e){(e=>{ +e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope, +delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={ +_wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope +}),(e=>{if(Array.isArray(e.begin)){ +if(e.skip||e.excludeBegin||e.returnBegin)throw W("skip, excludeBegin, returnBegin not compatible with beginScope: {}"), +K +;if("object"!=typeof e.beginScope||null===e.beginScope)throw W("beginScope must be object"), +K;F(e,e.begin,{key:"beginScope"}),e.begin=m(e.begin,{joinWith:""})}})(e),(e=>{ +if(Array.isArray(e.end)){ +if(e.skip||e.excludeEnd||e.returnEnd)throw W("skip, excludeEnd, returnEnd not compatible with endScope: {}"), +K +;if("object"!=typeof e.endScope||null===e.endScope)throw W("endScope must be object"), +K;F(e,e.end,{key:"endScope"}),e.end=m(e.end,{joinWith:""})}})(e)}function V(e){ +function t(t,n){ +return RegExp(l(t),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(n?"g":"")) +}class n{constructor(){ +this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0} +addRule(e,t){ +t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]), +this.matchAt+=p(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null) +;const e=this.regexes.map((e=>e[1]));this.matcherRe=t(m(e,{joinWith:"|" +}),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex +;const t=this.matcherRe.exec(e);if(!t)return null +;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n] +;return t.splice(0,n),Object.assign(t,i)}}class s{constructor(){ +this.rules=[],this.multiRegexes=[], +this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){ +if(this.multiRegexes[e])return this.multiRegexes[e];const t=new n +;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))), +t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){ +return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){ +this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){ +const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex +;let n=t.exec(e) +;if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{ +const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)} +return n&&(this.regexIndex+=n.position+1, +this.regexIndex===this.count&&this.considerAll()),n}} +if(e.compilerExtensions||(e.compilerExtensions=[]), +e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.") +;return e.classNameAliases=i(e.classNameAliases||{}),function n(o,r){const a=o +;if(o.isCompiled)return a +;[I,B,Z,D].forEach((e=>e(o,r))),e.compilerExtensions.forEach((e=>e(o,r))), +o.__beforeBegin=null,[T,L,P].forEach((e=>e(o,r))),o.isCompiled=!0;let c=null +;return"object"==typeof o.keywords&&o.keywords.$pattern&&(o.keywords=Object.assign({},o.keywords), +c=o.keywords.$pattern, +delete o.keywords.$pattern),c=c||/\w+/,o.keywords&&(o.keywords=$(o.keywords,e.case_insensitive)), +a.keywordPatternRe=t(c,!0), +r&&(o.begin||(o.begin=/\B|\b/),a.beginRe=t(a.begin),o.end||o.endsWithParent||(o.end=/\B|\b/), +o.end&&(a.endRe=t(a.end)), +a.terminatorEnd=l(a.end)||"",o.endsWithParent&&r.terminatorEnd&&(a.terminatorEnd+=(o.end?"|":"")+r.terminatorEnd)), +o.illegal&&(a.illegalRe=t(o.illegal)), +o.contains||(o.contains=[]),o.contains=[].concat(...o.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((t=>i(e,{ +variants:null},t)))),e.cachedVariants?e.cachedVariants:q(e)?i(e,{ +starts:e.starts?i(e.starts):null +}):Object.isFrozen(e)?i(e):e))("self"===e?o:e)))),o.contains.forEach((e=>{n(e,a) +})),o.starts&&n(o.starts,r),a.matcher=(e=>{const t=new s +;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin" +}))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end" +}),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t})(a),a}(e)}function q(e){ +return!!e&&(e.endsWithParent||q(e.starts))}class J extends Error{ +constructor(e,t){super(e),this.name="HTMLInjectionError",this.html=t}} +const Y=n,Q=i,ee=Symbol("nomatch"),te=n=>{ +const i=Object.create(null),s=Object.create(null),o=[];let r=!0 +;const a="Could not find the language '{}', did you forget to load/include a language module?",l={ +disableAutodetect:!0,name:"Plain text",contains:[]};let p={ +ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i, +languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-", +cssSelector:"pre code",languages:null,__emitter:c};function b(e){ +return p.noHighlightRe.test(e)}function m(e,t,n){let i="",s="" +;"object"==typeof t?(i=e, +n=t.ignoreIllegals,s=t.language):(G("10.7.0","highlight(lang, code, ...args) has been deprecated."), +G("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"), +s=e,i=t),void 0===n&&(n=!0);const o={code:i,language:s};N("before:highlight",o) +;const r=o.result?o.result:E(o.language,o.code,n) +;return r.code=o.code,N("after:highlight",r),r}function E(e,n,s,o){ +const c=Object.create(null);function l(){if(!N.keywords)return void M.addText(R) +;let e=0;N.keywordPatternRe.lastIndex=0;let t=N.keywordPatternRe.exec(R),n="" +;for(;t;){n+=R.substring(e,t.index) +;const s=_.case_insensitive?t[0].toLowerCase():t[0],o=(i=s,N.keywords[i]);if(o){ +const[e,i]=o +;if(M.addText(n),n="",c[s]=(c[s]||0)+1,c[s]<=7&&(j+=i),e.startsWith("_"))n+=t[0];else{ +const n=_.classNameAliases[e]||e;u(t[0],n)}}else n+=t[0] +;e=N.keywordPatternRe.lastIndex,t=N.keywordPatternRe.exec(R)}var i +;n+=R.substring(e),M.addText(n)}function g(){null!=N.subLanguage?(()=>{ +if(""===R)return;let e=null;if("string"==typeof N.subLanguage){ +if(!i[N.subLanguage])return void M.addText(R) +;e=E(N.subLanguage,R,!0,S[N.subLanguage]),S[N.subLanguage]=e._top +}else e=x(R,N.subLanguage.length?N.subLanguage:null) +;N.relevance>0&&(j+=e.relevance),M.__addSublanguage(e._emitter,e.language) +})():l(),R=""}function u(e,t){ +""!==e&&(M.startScope(t),M.addText(e),M.endScope())}function d(e,t){let n=1 +;const i=t.length-1;for(;n<=i;){if(!e._emit[n]){n++;continue} +const i=_.classNameAliases[e[n]]||e[n],s=t[n];i?u(s,i):(R=s,l(),R=""),n++}} +function h(e,t){ +return e.scope&&"string"==typeof e.scope&&M.openNode(_.classNameAliases[e.scope]||e.scope), +e.beginScope&&(e.beginScope._wrap?(u(R,_.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap), +R=""):e.beginScope._multi&&(d(e.beginScope,t),R="")),N=Object.create(e,{parent:{ +value:N}}),N}function f(e,n,i){let s=((e,t)=>{const n=e&&e.exec(t) +;return n&&0===n.index})(e.endRe,i);if(s){if(e["on:end"]){const i=new t(e) +;e["on:end"](n,i),i.isMatchIgnored&&(s=!1)}if(s){ +for(;e.endsParent&&e.parent;)e=e.parent;return e}} +if(e.endsWithParent)return f(e.parent,n,i)}function b(e){ +return 0===N.matcher.regexIndex?(R+=e[0],1):(T=!0,0)}function m(e){ +const t=e[0],i=n.substring(e.index),s=f(N,e,i);if(!s)return ee;const o=N +;N.endScope&&N.endScope._wrap?(g(), +u(t,N.endScope._wrap)):N.endScope&&N.endScope._multi?(g(), +d(N.endScope,e)):o.skip?R+=t:(o.returnEnd||o.excludeEnd||(R+=t), +g(),o.excludeEnd&&(R=t));do{ +N.scope&&M.closeNode(),N.skip||N.subLanguage||(j+=N.relevance),N=N.parent +}while(N!==s.parent);return s.starts&&h(s.starts,e),o.returnEnd?0:t.length} +let w={};function y(i,o){const a=o&&o[0];if(R+=i,null==a)return g(),0 +;if("begin"===w.type&&"end"===o.type&&w.index===o.index&&""===a){ +if(R+=n.slice(o.index,o.index+1),!r){const t=Error(`0 width match regex (${e})`) +;throw t.languageName=e,t.badRule=w.rule,t}return 1} +if(w=o,"begin"===o.type)return(e=>{ +const n=e[0],i=e.rule,s=new t(i),o=[i.__beforeBegin,i["on:begin"]] +;for(const t of o)if(t&&(t(e,s),s.isMatchIgnored))return b(n) +;return i.skip?R+=n:(i.excludeBegin&&(R+=n), +g(),i.returnBegin||i.excludeBegin||(R=n)),h(i,e),i.returnBegin?0:n.length})(o) +;if("illegal"===o.type&&!s){ +const e=Error('Illegal lexeme "'+a+'" for mode "'+(N.scope||"")+'"') +;throw e.mode=N,e}if("end"===o.type){const e=m(o);if(e!==ee)return e} +if("illegal"===o.type&&""===a)return 1 +;if(I>1e5&&I>3*o.index)throw Error("potential infinite loop, way more iterations than matches") +;return R+=a,a.length}const _=O(e) +;if(!_)throw W(a.replace("{}",e)),Error('Unknown language: "'+e+'"') +;const v=V(_);let k="",N=o||v;const S={},M=new p.__emitter(p);(()=>{const e=[] +;for(let t=N;t!==_;t=t.parent)t.scope&&e.unshift(t.scope) +;e.forEach((e=>M.openNode(e)))})();let R="",j=0,A=0,I=0,T=!1;try{ +if(_.__emitTokens)_.__emitTokens(n,M);else{for(N.matcher.considerAll();;){ +I++,T?T=!1:N.matcher.considerAll(),N.matcher.lastIndex=A +;const e=N.matcher.exec(n);if(!e)break;const t=y(n.substring(A,e.index),e) +;A=e.index+t}y(n.substring(A))}return M.finalize(),k=M.toHTML(),{language:e, +value:k,relevance:j,illegal:!1,_emitter:M,_top:N}}catch(t){ +if(t.message&&t.message.includes("Illegal"))return{language:e,value:Y(n), +illegal:!0,relevance:0,_illegalBy:{message:t.message,index:A, +context:n.slice(A-100,A+100),mode:t.mode,resultSoFar:k},_emitter:M};if(r)return{ +language:e,value:Y(n),illegal:!1,relevance:0,errorRaised:t,_emitter:M,_top:N} +;throw t}}function x(e,t){t=t||p.languages||Object.keys(i);const n=(e=>{ +const t={value:Y(e),illegal:!1,relevance:0,_top:l,_emitter:new p.__emitter(p)} +;return t._emitter.addText(e),t})(e),s=t.filter(O).filter(k).map((t=>E(t,e,!1))) +;s.unshift(n);const o=s.sort(((e,t)=>{ +if(e.relevance!==t.relevance)return t.relevance-e.relevance +;if(e.language&&t.language){if(O(e.language).supersetOf===t.language)return 1 +;if(O(t.language).supersetOf===e.language)return-1}return 0})),[r,a]=o,c=r +;return c.secondBest=a,c}function w(e){let t=null;const n=(e=>{ +let t=e.className+" ";t+=e.parentNode?e.parentNode.className:"" +;const n=p.languageDetectRe.exec(t);if(n){const t=O(n[1]) +;return t||(X(a.replace("{}",n[1])), +X("Falling back to no-highlight mode for this block.",e)),t?n[1]:"no-highlight"} +return t.split(/\s+/).find((e=>b(e)||O(e)))})(e);if(b(n))return +;if(N("before:highlightElement",{el:e,language:n +}),e.dataset.highlighted)return void console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",e) +;if(e.children.length>0&&(p.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."), +console.warn("https://github.com/highlightjs/highlight.js/wiki/security"), +console.warn("The element with unescaped HTML:"), +console.warn(e)),p.throwUnescapedHTML))throw new J("One of your code blocks includes unescaped HTML.",e.innerHTML) +;t=e;const i=t.textContent,o=n?m(i,{language:n,ignoreIllegals:!0}):x(i) +;e.innerHTML=o.value,e.dataset.highlighted="yes",((e,t,n)=>{const i=t&&s[t]||n +;e.classList.add("hljs"),e.classList.add("language-"+i) +})(e,n,o.language),e.result={language:o.language,re:o.relevance, +relevance:o.relevance},o.secondBest&&(e.secondBest={ +language:o.secondBest.language,relevance:o.secondBest.relevance +}),N("after:highlightElement",{el:e,result:o,text:i})}let y=!1;function _(){ +"loading"!==document.readyState?document.querySelectorAll(p.cssSelector).forEach(w):y=!0 +}function O(e){return e=(e||"").toLowerCase(),i[e]||i[s[e]]} +function v(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{ +s[e.toLowerCase()]=t}))}function k(e){const t=O(e) +;return t&&!t.disableAutodetect}function N(e,t){const n=e;o.forEach((e=>{ +e[n]&&e[n](t)}))} +"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{ +y&&_()}),!1),Object.assign(n,{highlight:m,highlightAuto:x,highlightAll:_, +highlightElement:w, +highlightBlock:e=>(G("10.7.0","highlightBlock will be removed entirely in v12.0"), +G("10.7.0","Please use highlightElement now."),w(e)),configure:e=>{p=Q(p,e)}, +initHighlighting:()=>{ +_(),G("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")}, +initHighlightingOnLoad:()=>{ +_(),G("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.") +},registerLanguage:(e,t)=>{let s=null;try{s=t(n)}catch(t){ +if(W("Language definition for '{}' could not be registered.".replace("{}",e)), +!r)throw t;W(t),s=l} +s.name||(s.name=e),i[e]=s,s.rawDefinition=t.bind(null,n),s.aliases&&v(s.aliases,{ +languageName:e})},unregisterLanguage:e=>{delete i[e] +;for(const t of Object.keys(s))s[t]===e&&delete s[t]}, +listLanguages:()=>Object.keys(i),getLanguage:O,registerAliases:v, +autoDetection:k,inherit:Q,addPlugin:e=>{(e=>{ +e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{ +e["before:highlightBlock"](Object.assign({block:t.el},t)) +}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{ +e["after:highlightBlock"](Object.assign({block:t.el},t))})})(e),o.push(e)}, +removePlugin:e=>{const t=o.indexOf(e);-1!==t&&o.splice(t,1)}}),n.debugMode=()=>{ +r=!1},n.safeMode=()=>{r=!0},n.versionString="11.9.0",n.regex={concat:h, +lookahead:g,either:f,optional:d,anyNumberOfTimes:u} +;for(const t in j)"object"==typeof j[t]&&e(j[t]);return Object.assign(n,j),n +},ne=te({});return ne.newInstance=()=>te({}),ne}() +;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);/*! `bash` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const s=e.regex,t={},n={begin:/\$\{/, +end:/\}/,contains:["self",{begin:/:-/,contains:[t]}]};Object.assign(t,{ +className:"variable",variants:[{ +begin:s.concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},n]});const a={ +className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE] +},i=e.inherit(e.COMMENT(),{match:[/(^|\s)/,/#.*$/],scope:{2:"comment"}}),c={ +begin:/<<-?\s*(?=\w+)/,starts:{contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/, +end:/(\w+)/,className:"string"})]}},o={className:"string",begin:/"/,end:/"/, +contains:[e.BACKSLASH_ESCAPE,t,a]};a.contains.push(o);const r={begin:/\$?\(\(/, +end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,t] +},l=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10 +}),m={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0, +contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{ +name:"Bash",aliases:["sh"],keywords:{$pattern:/\b[a-z][a-z0-9._-]+\b/, +keyword:["if","then","else","elif","fi","for","while","until","in","do","done","case","esac","function","select"], +literal:["true","false"], +built_in:["break","cd","continue","eval","exec","exit","export","getopts","hash","pwd","readonly","return","shift","test","times","trap","umask","unset","alias","bind","builtin","caller","command","declare","echo","enable","help","let","local","logout","mapfile","printf","read","readarray","source","type","typeset","ulimit","unalias","set","shopt","autoload","bg","bindkey","bye","cap","chdir","clone","comparguments","compcall","compctl","compdescribe","compfiles","compgroups","compquote","comptags","comptry","compvalues","dirs","disable","disown","echotc","echoti","emulate","fc","fg","float","functions","getcap","getln","history","integer","jobs","kill","limit","log","noglob","popd","print","pushd","pushln","rehash","sched","setcap","setopt","stat","suspend","ttyctl","unfunction","unhash","unlimit","unsetopt","vared","wait","whence","where","which","zcompile","zformat","zftp","zle","zmodload","zparseopts","zprof","zpty","zregexparse","zsocket","zstyle","ztcp","chcon","chgrp","chown","chmod","cp","dd","df","dir","dircolors","ln","ls","mkdir","mkfifo","mknod","mktemp","mv","realpath","rm","rmdir","shred","sync","touch","truncate","vdir","b2sum","base32","base64","cat","cksum","comm","csplit","cut","expand","fmt","fold","head","join","md5sum","nl","numfmt","od","paste","ptx","pr","sha1sum","sha224sum","sha256sum","sha384sum","sha512sum","shuf","sort","split","sum","tac","tail","tr","tsort","unexpand","uniq","wc","arch","basename","chroot","date","dirname","du","echo","env","expr","factor","groups","hostid","id","link","logname","nice","nohup","nproc","pathchk","pinky","printenv","printf","pwd","readlink","runcon","seq","sleep","stat","stdbuf","stty","tee","test","timeout","tty","uname","unlink","uptime","users","who","whoami","yes"] +},contains:[l,e.SHEBANG(),m,r,i,c,{match:/(\/[a-z._-]+)+/},o,{match:/\\"/},{ +className:"string",begin:/'/,end:/'/},{match:/\\'/},t]}}})() +;hljs.registerLanguage("bash",e)})();/*! `c` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const n=e.regex,t=e.COMMENT("//","$",{ +contains:[{begin:/\\\n/}] +}),s="decltype\\(auto\\)",a="[a-zA-Z_]\\w*::",r="("+s+"|"+n.optional(a)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",i={ +className:"type",variants:[{begin:"\\b[a-z\\d_]*_t\\b"},{ +match:/\batomic_[a-z]{3,6}\b/}]},l={className:"string",variants:[{ +begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{ +begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", +end:"'",illegal:"."},e.END_SAME_AS_BEGIN({ +begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},o={ +className:"number",variants:[{begin:"\\b(0b[01']+)"},{ +begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)" +},{ +begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" +}],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{ +keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" +},contains:[{begin:/\\\n/,relevance:0},e.inherit(l,{className:"string"}),{ +className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},d={ +className:"title",begin:n.optional(a)+e.IDENT_RE,relevance:0 +},g=n.optional(a)+e.IDENT_RE+"\\s*\\(",u={ +keyword:["asm","auto","break","case","continue","default","do","else","enum","extern","for","fortran","goto","if","inline","register","restrict","return","sizeof","struct","switch","typedef","union","volatile","while","_Alignas","_Alignof","_Atomic","_Generic","_Noreturn","_Static_assert","_Thread_local","alignas","alignof","noreturn","static_assert","thread_local","_Pragma"], +type:["float","double","signed","unsigned","int","short","long","char","void","_Bool","_Complex","_Imaginary","_Decimal32","_Decimal64","_Decimal128","const","static","complex","bool","imaginary"], +literal:"true false NULL", +built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr" +},m=[c,i,t,e.C_BLOCK_COMMENT_MODE,o,l],_={variants:[{begin:/=/,end:/;/},{ +begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}], +keywords:u,contains:m.concat([{begin:/\(/,end:/\)/,keywords:u, +contains:m.concat(["self"]),relevance:0}]),relevance:0},p={ +begin:"("+r+"[\\*&\\s]+)+"+g,returnBegin:!0,end:/[{;=]/,excludeEnd:!0, +keywords:u,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:s,keywords:u,relevance:0},{ +begin:g,returnBegin:!0,contains:[e.inherit(d,{className:"title.function"})], +relevance:0},{relevance:0,match:/,/},{className:"params",begin:/\(/,end:/\)/, +keywords:u,relevance:0,contains:[t,e.C_BLOCK_COMMENT_MODE,l,o,i,{begin:/\(/, +end:/\)/,keywords:u,relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,l,o,i] +}]},i,t,e.C_BLOCK_COMMENT_MODE,c]};return{name:"C",aliases:["h"],keywords:u, +disableAutodetect:!0,illegal:"=]/,contains:[{ +beginKeywords:"final class struct"},e.TITLE_MODE]}]),exports:{preprocessor:c, +strings:l,keywords:u}}}})();hljs.registerLanguage("c",e)})();/*! `cpp` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const t=e.regex,a=e.COMMENT("//","$",{ +contains:[{begin:/\\\n/}] +}),n="decltype\\(auto\\)",r="[a-zA-Z_]\\w*::",i="(?!struct)("+n+"|"+t.optional(r)+"[a-zA-Z_]\\w*"+t.optional("<[^<>]+>")+")",s={ +className:"type",begin:"\\b[a-z\\d_]*_t\\b"},c={className:"string",variants:[{ +begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{ +begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)", +end:"'",illegal:"."},e.END_SAME_AS_BEGIN({ +begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},o={ +className:"number",variants:[{ +begin:"[+-]?(?:(?:[0-9](?:'?[0-9])*\\.(?:[0-9](?:'?[0-9])*)?|\\.[0-9](?:'?[0-9])*)(?:[Ee][+-]?[0-9](?:'?[0-9])*)?|[0-9](?:'?[0-9])*[Ee][+-]?[0-9](?:'?[0-9])*|0[Xx](?:[0-9A-Fa-f](?:'?[0-9A-Fa-f])*(?:\\.(?:[0-9A-Fa-f](?:'?[0-9A-Fa-f])*)?)?|\\.[0-9A-Fa-f](?:'?[0-9A-Fa-f])*)[Pp][+-]?[0-9](?:'?[0-9])*)(?:[Ff](?:16|32|64|128)?|(BF|bf)16|[Ll]|)" +},{ +begin:"[+-]?\\b(?:0[Bb][01](?:'?[01])*|0[Xx][0-9A-Fa-f](?:'?[0-9A-Fa-f])*|0(?:'?[0-7])*|[1-9](?:'?[0-9])*)(?:[Uu](?:LL?|ll?)|[Uu][Zz]?|(?:LL?|ll?)[Uu]?|[Zz][Uu]|)" +}],relevance:0},l={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{ +keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include" +},contains:[{begin:/\\\n/,relevance:0},e.inherit(c,{className:"string"}),{ +className:"string",begin:/<.*?>/},a,e.C_BLOCK_COMMENT_MODE]},u={ +className:"title",begin:t.optional(r)+e.IDENT_RE,relevance:0 +},d=t.optional(r)+e.IDENT_RE+"\\s*\\(",p={ +type:["bool","char","char16_t","char32_t","char8_t","double","float","int","long","short","void","wchar_t","unsigned","signed","const","static"], +keyword:["alignas","alignof","and","and_eq","asm","atomic_cancel","atomic_commit","atomic_noexcept","auto","bitand","bitor","break","case","catch","class","co_await","co_return","co_yield","compl","concept","const_cast|10","consteval","constexpr","constinit","continue","decltype","default","delete","do","dynamic_cast|10","else","enum","explicit","export","extern","false","final","for","friend","goto","if","import","inline","module","mutable","namespace","new","noexcept","not","not_eq","nullptr","operator","or","or_eq","override","private","protected","public","reflexpr","register","reinterpret_cast|10","requires","return","sizeof","static_assert","static_cast|10","struct","switch","synchronized","template","this","thread_local","throw","transaction_safe","transaction_safe_dynamic","true","try","typedef","typeid","typename","union","using","virtual","volatile","while","xor","xor_eq"], +literal:["NULL","false","nullopt","nullptr","true"],built_in:["_Pragma"], +_type_hints:["any","auto_ptr","barrier","binary_semaphore","bitset","complex","condition_variable","condition_variable_any","counting_semaphore","deque","false_type","future","imaginary","initializer_list","istringstream","jthread","latch","lock_guard","multimap","multiset","mutex","optional","ostringstream","packaged_task","pair","promise","priority_queue","queue","recursive_mutex","recursive_timed_mutex","scoped_lock","set","shared_future","shared_lock","shared_mutex","shared_timed_mutex","shared_ptr","stack","string_view","stringstream","timed_mutex","thread","true_type","tuple","unique_lock","unique_ptr","unordered_map","unordered_multimap","unordered_multiset","unordered_set","variant","vector","weak_ptr","wstring","wstring_view"] +},_={className:"function.dispatch",relevance:0,keywords:{ +_hint:["abort","abs","acos","apply","as_const","asin","atan","atan2","calloc","ceil","cerr","cin","clog","cos","cosh","cout","declval","endl","exchange","exit","exp","fabs","floor","fmod","forward","fprintf","fputs","free","frexp","fscanf","future","invoke","isalnum","isalpha","iscntrl","isdigit","isgraph","islower","isprint","ispunct","isspace","isupper","isxdigit","labs","launder","ldexp","log","log10","make_pair","make_shared","make_shared_for_overwrite","make_tuple","make_unique","malloc","memchr","memcmp","memcpy","memset","modf","move","pow","printf","putchar","puts","realloc","scanf","sin","sinh","snprintf","sprintf","sqrt","sscanf","std","stderr","stdin","stdout","strcat","strchr","strcmp","strcpy","strcspn","strlen","strncat","strncmp","strncpy","strpbrk","strrchr","strspn","strstr","swap","tan","tanh","terminate","to_underlying","tolower","toupper","vfprintf","visit","vprintf","vsprintf"] +}, +begin:t.concat(/\b/,/(?!decltype)/,/(?!if)/,/(?!for)/,/(?!switch)/,/(?!while)/,e.IDENT_RE,t.lookahead(/(<[^<>]+>|)\s*\(/)) +},m=[_,l,s,a,e.C_BLOCK_COMMENT_MODE,o,c],f={variants:[{begin:/=/,end:/;/},{ +begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}], +keywords:p,contains:m.concat([{begin:/\(/,end:/\)/,keywords:p, +contains:m.concat(["self"]),relevance:0}]),relevance:0},g={className:"function", +begin:"("+i+"[\\*&\\s]+)+"+d,returnBegin:!0,end:/[{;=]/,excludeEnd:!0, +keywords:p,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:n,keywords:p,relevance:0},{ +begin:d,returnBegin:!0,contains:[u],relevance:0},{begin:/::/,relevance:0},{ +begin:/:/,endsWithParent:!0,contains:[c,o]},{relevance:0,match:/,/},{ +className:"params",begin:/\(/,end:/\)/,keywords:p,relevance:0, +contains:[a,e.C_BLOCK_COMMENT_MODE,c,o,s,{begin:/\(/,end:/\)/,keywords:p, +relevance:0,contains:["self",a,e.C_BLOCK_COMMENT_MODE,c,o,s]}] +},s,a,e.C_BLOCK_COMMENT_MODE,l]};return{name:"C++", +aliases:["cc","c++","h++","hpp","hh","hxx","cxx"],keywords:p,illegal:"",keywords:p,contains:["self",s]},{begin:e.IDENT_RE+"::",keywords:p},{ +match:[/\b(?:enum(?:\s+(?:class|struct))?|class|struct|union)/,/\s+/,/\w+/], +className:{1:"keyword",3:"title.class"}}])}}})();hljs.registerLanguage("cpp",e) +})();/*! `csharp` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const n={ +keyword:["abstract","as","base","break","case","catch","class","const","continue","do","else","event","explicit","extern","finally","fixed","for","foreach","goto","if","implicit","in","interface","internal","is","lock","namespace","new","operator","out","override","params","private","protected","public","readonly","record","ref","return","scoped","sealed","sizeof","stackalloc","static","struct","switch","this","throw","try","typeof","unchecked","unsafe","using","virtual","void","volatile","while"].concat(["add","alias","and","ascending","async","await","by","descending","equals","from","get","global","group","init","into","join","let","nameof","not","notnull","on","or","orderby","partial","remove","select","set","unmanaged","value|0","var","when","where","with","yield"]), +built_in:["bool","byte","char","decimal","delegate","double","dynamic","enum","float","int","long","nint","nuint","object","sbyte","short","string","ulong","uint","ushort"], +literal:["default","false","null","true"]},a=e.inherit(e.TITLE_MODE,{ +begin:"[a-zA-Z](\\.?\\w)*"}),i={className:"number",variants:[{ +begin:"\\b(0b[01']+)"},{ +begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{ +begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)" +}],relevance:0},s={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}] +},t=e.inherit(s,{illegal:/\n/}),r={className:"subst",begin:/\{/,end:/\}/, +keywords:n},l=e.inherit(r,{illegal:/\n/}),c={className:"string",begin:/\$"/, +end:'"',illegal:/\n/,contains:[{begin:/\{\{/},{begin:/\}\}/ +},e.BACKSLASH_ESCAPE,l]},o={className:"string",begin:/\$@"/,end:'"',contains:[{ +begin:/\{\{/},{begin:/\}\}/},{begin:'""'},r]},d=e.inherit(o,{illegal:/\n/, +contains:[{begin:/\{\{/},{begin:/\}\}/},{begin:'""'},l]}) +;r.contains=[o,c,s,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,i,e.C_BLOCK_COMMENT_MODE], +l.contains=[d,c,t,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,i,e.inherit(e.C_BLOCK_COMMENT_MODE,{ +illegal:/\n/})];const g={variants:[o,c,s,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE] +},E={begin:"<",end:">",contains:[{beginKeywords:"in out"},a] +},_=e.IDENT_RE+"(<"+e.IDENT_RE+"(\\s*,\\s*"+e.IDENT_RE+")*>)?(\\[\\])?",b={ +begin:"@"+e.IDENT_RE,relevance:0};return{name:"C#",aliases:["cs","c#"], +keywords:n,illegal:/::/,contains:[e.COMMENT("///","$",{returnBegin:!0, +contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{ +begin:"\x3c!--|--\x3e"},{begin:""}]}] +}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#", +end:"$",keywords:{ +keyword:"if else elif endif define undef warning error line region endregion pragma checksum" +}},g,i,{beginKeywords:"class interface",relevance:0,end:/[{;=]/, +illegal:/[^\s:,]/,contains:[{beginKeywords:"where class" +},a,E,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace", +relevance:0,end:/[{;=]/,illegal:/[^\s:]/, +contains:[a,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{ +beginKeywords:"record",relevance:0,end:/[{;=]/,illegal:/[^\s:]/, +contains:[a,E,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"meta", +begin:"^\\s*\\[(?=[\\w])",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{ +className:"string",begin:/"/,end:/"/}]},{ +beginKeywords:"new return throw await else",relevance:0},{className:"function", +begin:"("+_+"\\s+)+"+e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0, +end:/\s*[{;=]/,excludeEnd:!0,keywords:n,contains:[{ +beginKeywords:"public private protected static internal protected abstract async extern override unsafe virtual new sealed partial", +relevance:0},{begin:e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0, +contains:[e.TITLE_MODE,E],relevance:0},{match:/\(\)/},{className:"params", +begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,relevance:0, +contains:[g,i,e.C_BLOCK_COMMENT_MODE] +},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},b]}}})() +;hljs.registerLanguage("csharp",e)})();/*! `css` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict" +;const e=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video","defs","g","marker","mask","pattern","svg","switch","symbol","feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feFlood","feGaussianBlur","feImage","feMerge","feMorphology","feOffset","feSpecularLighting","feTile","feTurbulence","linearGradient","radialGradient","stop","circle","ellipse","image","line","path","polygon","polyline","rect","text","use","textPath","tspan","foreignObject","clipPath"],r=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"].sort().reverse(),o=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"].sort().reverse(),t=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"].sort().reverse(),i=["align-content","align-items","align-self","alignment-baseline","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","baseline-shift","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","cx","cy","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","dominant-baseline","empty-cells","enable-background","fill","fill-opacity","fill-rule","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","flood-color","flood-opacity","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-horizontal","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","kerning","justify-content","left","letter-spacing","lighting-color","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","marker","marker-end","marker-mid","marker-start","mask","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","r","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","shape-rendering","stop-color","stop-opacity","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","speak","speak-as","src","tab-size","table-layout","text-anchor","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vector-effect","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","x","y","z-index"].sort().reverse() +;return n=>{const a=n.regex,l=(e=>({IMPORTANT:{scope:"meta",begin:"!important"}, +BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{scope:"number", +begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/},FUNCTION_DISPATCH:{ +className:"built_in",begin:/[\w-]+(?=\()/},ATTRIBUTE_SELECTOR_MODE:{ +scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", +contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ +scope:"number", +begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", +relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z_][A-Za-z0-9_-]*/} +}))(n),s=[n.APOS_STRING_MODE,n.QUOTE_STRING_MODE];return{name:"CSS", +case_insensitive:!0,illegal:/[=|'\$]/,keywords:{keyframePosition:"from to"}, +classNameAliases:{keyframePosition:"selector-tag"},contains:[l.BLOCK_COMMENT,{ +begin:/-(webkit|moz|ms|o)-(?=[a-z])/},l.CSS_NUMBER_MODE,{ +className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0},{ +className:"selector-class",begin:"\\.[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0 +},l.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{ +begin:":("+o.join("|")+")"},{begin:":(:)?("+t.join("|")+")"}]},l.CSS_VARIABLE,{ +className:"attribute",begin:"\\b("+i.join("|")+")\\b"},{begin:/:/,end:/[;}{]/, +contains:[l.BLOCK_COMMENT,l.HEXCOLOR,l.IMPORTANT,l.CSS_NUMBER_MODE,...s,{ +begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri" +},contains:[...s,{className:"string",begin:/[^)]/,endsWithParent:!0, +excludeEnd:!0}]},l.FUNCTION_DISPATCH]},{begin:a.lookahead(/@/),end:"[{;]", +relevance:0,illegal:/:/,contains:[{className:"keyword",begin:/@-?\w[\w]*(-\w+)*/ +},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:{ +$pattern:/[a-z-]+/,keyword:"and or not only",attribute:r.join(" ")},contains:[{ +begin:/[a-z-]+(?=:)/,className:"attribute"},...s,l.CSS_NUMBER_MODE]}]},{ +className:"selector-tag",begin:"\\b("+e.join("|")+")\\b"}]}}})() +;hljs.registerLanguage("css",e)})();/*! `diff` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const a=e.regex;return{name:"Diff", +aliases:["patch"],contains:[{className:"meta",relevance:10, +match:a.either(/^@@ +-\d+,\d+ +\+\d+,\d+ +@@/,/^\*\*\* +\d+,\d+ +\*\*\*\*$/,/^--- +\d+,\d+ +----$/) +},{className:"comment",variants:[{ +begin:a.either(/Index: /,/^index/,/={3,}/,/^-{3}/,/^\*{3} /,/^\+{3}/,/^diff --git/), +end:/$/},{match:/^\*{15}$/}]},{className:"addition",begin:/^\+/,end:/$/},{ +className:"deletion",begin:/^-/,end:/$/},{className:"addition",begin:/^!/, +end:/$/}]}}})();hljs.registerLanguage("diff",e)})();/*! `go` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const n={ +keyword:["break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","switch","type","var"], +type:["bool","byte","complex64","complex128","error","float32","float64","int8","int16","int32","int64","string","uint8","uint16","uint32","uint64","int","uint","uintptr","rune"], +literal:["true","false","iota","nil"], +built_in:["append","cap","close","complex","copy","imag","len","make","new","panic","print","println","real","recover","delete"] +};return{name:"Go",aliases:["golang"],keywords:n,illegal:"{var e=(()=>{"use strict";return e=>{const a=e.regex;return{name:"GraphQL", +aliases:["gql"],case_insensitive:!0,disableAutodetect:!1,keywords:{ +keyword:["query","mutation","subscription","type","input","schema","directive","interface","union","scalar","fragment","enum","on"], +literal:["true","false","null"]}, +contains:[e.HASH_COMMENT_MODE,e.QUOTE_STRING_MODE,e.NUMBER_MODE,{ +scope:"punctuation",match:/[.]{3}/,relevance:0},{scope:"punctuation", +begin:/[\!\(\)\:\=\[\]\{\|\}]{1}/,relevance:0},{scope:"variable",begin:/\$/, +end:/\W/,excludeEnd:!0,relevance:0},{scope:"meta",match:/@\w+/,excludeEnd:!0},{ +scope:"symbol",begin:a.concat(/[_A-Za-z][_0-9A-Za-z]*/,a.lookahead(/\s*:/)), +relevance:0}],illegal:[/[;<']/,/BEGIN/]}}})();hljs.registerLanguage("graphql",e) +})();/*! `ini` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const n=e.regex,a={className:"number", +relevance:0,variants:[{begin:/([+-]+)?[\d]+_[\d_]+/},{begin:e.NUMBER_RE}] +},s=e.COMMENT();s.variants=[{begin:/;/,end:/$/},{begin:/#/,end:/$/}];const i={ +className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{begin:/\$\{(.*?)\}/ +}]},t={className:"literal",begin:/\bon|off|true|false|yes|no\b/},r={ +className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{begin:"'''", +end:"'''",relevance:10},{begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"' +},{begin:"'",end:"'"}]},l={begin:/\[/,end:/\]/,contains:[s,t,i,r,a,"self"], +relevance:0},c=n.either(/[A-Za-z0-9_-]+/,/"(\\"|[^"])*"/,/'[^']*'/);return{ +name:"TOML, also INI",aliases:["toml"],case_insensitive:!0,illegal:/\S/, +contains:[s,{className:"section",begin:/\[+/,end:/\]+/},{ +begin:n.concat(c,"(\\s*\\.\\s*",c,")*",n.lookahead(/\s*=\s*[^#\s]/)), +className:"attr",starts:{end:/$/,contains:[s,l,t,i,r,a]}}]}}})() +;hljs.registerLanguage("ini",e)})();/*! `java` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict" +;var e="[0-9](_*[0-9])*",a=`\\.(${e})`,n="[0-9a-fA-F](_*[0-9a-fA-F])*",s={ +className:"number",variants:[{ +begin:`(\\b(${e})((${a})|\\.)?|(${a}))[eE][+-]?(${e})[fFdD]?\\b`},{ +begin:`\\b(${e})((${a})[fFdD]?\\b|\\.([fFdD]\\b)?)`},{begin:`(${a})[fFdD]?\\b` +},{begin:`\\b(${e})[fFdD]\\b`},{ +begin:`\\b0[xX]((${n})\\.?|(${n})?\\.(${n}))[pP][+-]?(${e})[fFdD]?\\b`},{ +begin:"\\b(0|[1-9](_*[0-9])*)[lL]?\\b"},{begin:`\\b0[xX](${n})[lL]?\\b`},{ +begin:"\\b0(_*[0-7])*[lL]?\\b"},{begin:"\\b0[bB][01](_*[01])*[lL]?\\b"}], +relevance:0};function t(e,a,n){return-1===n?"":e.replace(a,(s=>t(e,a,n-1)))} +return e=>{ +const a=e.regex,n="[\xc0-\u02b8a-zA-Z_$][\xc0-\u02b8a-zA-Z_$0-9]*",i=n+t("(?:<"+n+"~~~(?:\\s*,\\s*"+n+"~~~)*>)?",/~~~/g,2),r={ +keyword:["synchronized","abstract","private","var","static","if","const ","for","while","strictfp","finally","protected","import","native","final","void","enum","else","break","transient","catch","instanceof","volatile","case","assert","package","default","public","try","switch","continue","throws","protected","public","private","module","requires","exports","do","sealed","yield","permits"], +literal:["false","true","null"], +type:["char","boolean","long","float","int","byte","short","double"], +built_in:["super","this"]},l={className:"meta",begin:"@"+n,contains:[{ +begin:/\(/,end:/\)/,contains:["self"]}]},c={className:"params",begin:/\(/, +end:/\)/,keywords:r,relevance:0,contains:[e.C_BLOCK_COMMENT_MODE],endsParent:!0} +;return{name:"Java",aliases:["jsp"],keywords:r,illegal:/<\/|#/, +contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/, +relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),{ +begin:/import java\.[a-z]+\./,keywords:"import",relevance:2 +},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{begin:/"""/,end:/"""/, +className:"string",contains:[e.BACKSLASH_ESCAPE] +},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{ +match:[/\b(?:class|interface|enum|extends|implements|new)/,/\s+/,n],className:{ +1:"keyword",3:"title.class"}},{match:/non-sealed/,scope:"keyword"},{ +begin:[a.concat(/(?!else)/,n),/\s+/,n,/\s+/,/=(?!=)/],className:{1:"type", +3:"variable",5:"operator"}},{begin:[/record/,/\s+/,n],className:{1:"keyword", +3:"title.class"},contains:[c,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{ +beginKeywords:"new throw return else",relevance:0},{ +begin:["(?:"+i+"\\s+)",e.UNDERSCORE_IDENT_RE,/\s*(?=\()/],className:{ +2:"title.function"},keywords:r,contains:[{className:"params",begin:/\(/, +end:/\)/,keywords:r,relevance:0, +contains:[l,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,s,e.C_BLOCK_COMMENT_MODE] +},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},s,l]}}})() +;hljs.registerLanguage("java",e)})();/*! `javascript` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict" +;const e="[A-Za-z$_][0-9A-Za-z$_]*",n=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],a=["true","false","null","undefined","NaN","Infinity"],t=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],s=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],r=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],c=["arguments","this","super","console","window","document","localStorage","sessionStorage","module","global"],i=[].concat(r,t,s) +;return o=>{const l=o.regex,b=e,d={begin:/<[A-Za-z0-9\\._:-]+/, +end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{ +const a=e[0].length+e.index,t=e.input[a] +;if("<"===t||","===t)return void n.ignoreMatch();let s +;">"===t&&(((e,{after:n})=>{const a="",$={ +match:[/const|var|let/,/\s+/,b,/\s*/,/=\s*/,/(async\s*)?/,l.lookahead(B)], +keywords:"async",className:{1:"keyword",3:"title.function"},contains:[R]} +;return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:g,exports:{ +PARAMS_CONTAINS:w,CLASS_REFERENCE:k},illegal:/#(?![$_A-z])/, +contains:[o.SHEBANG({label:"shebang",binary:"node",relevance:5}),{ +label:"use_strict",className:"meta",relevance:10, +begin:/^\s*['"]use (strict|asm)['"]/ +},o.APOS_STRING_MODE,o.QUOTE_STRING_MODE,h,N,_,f,v,{match:/\$\d+/},A,k,{ +className:"attr",begin:b+l.lookahead(":"),relevance:0},$,{ +begin:"("+o.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*", +keywords:"return throw case",relevance:0,contains:[v,o.REGEXP_MODE,{ +className:"function",begin:B,returnBegin:!0,end:"\\s*=>",contains:[{ +className:"params",variants:[{begin:o.UNDERSCORE_IDENT_RE,relevance:0},{ +className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0, +excludeEnd:!0,keywords:g,contains:w}]}]},{begin:/,/,relevance:0},{match:/\s+/, +relevance:0},{variants:[{begin:"<>",end:""},{ +match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:d.begin, +"on:begin":d.isTrulyOpeningTag,end:d.end}],subLanguage:"xml",contains:[{ +begin:d.begin,end:d.end,skip:!0,contains:["self"]}]}]},I,{ +beginKeywords:"while if switch catch for"},{ +begin:"\\b(?!function)"+o.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", +returnBegin:!0,label:"func.def",contains:[R,o.inherit(o.TITLE_MODE,{begin:b, +className:"title.function"})]},{match:/\.\.\./,relevance:0},C,{match:"\\$"+b, +relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"}, +contains:[R]},x,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, +className:"variable.constant"},O,M,{match:/\$[(.]/}]}}})() +;hljs.registerLanguage("javascript",e)})();/*! `json` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const a=["true","false","null"],n={ +scope:"literal",beginKeywords:a.join(" ")};return{name:"JSON",keywords:{ +literal:a},contains:[{className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/, +relevance:1.01},{match:/[{}[\],:]/,className:"punctuation",relevance:0 +},e.QUOTE_STRING_MODE,n,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE], +illegal:"\\S"}}})();hljs.registerLanguage("json",e)})();/*! `kotlin` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict" +;var e="[0-9](_*[0-9])*",n=`\\.(${e})`,a="[0-9a-fA-F](_*[0-9a-fA-F])*",i={ +className:"number",variants:[{ +begin:`(\\b(${e})((${n})|\\.)?|(${n}))[eE][+-]?(${e})[fFdD]?\\b`},{ +begin:`\\b(${e})((${n})[fFdD]?\\b|\\.([fFdD]\\b)?)`},{begin:`(${n})[fFdD]?\\b` +},{begin:`\\b(${e})[fFdD]\\b`},{ +begin:`\\b0[xX]((${a})\\.?|(${a})?\\.(${a}))[pP][+-]?(${e})[fFdD]?\\b`},{ +begin:"\\b(0|[1-9](_*[0-9])*)[lL]?\\b"},{begin:`\\b0[xX](${a})[lL]?\\b`},{ +begin:"\\b0(_*[0-7])*[lL]?\\b"},{begin:"\\b0[bB][01](_*[01])*[lL]?\\b"}], +relevance:0};return e=>{const n={ +keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual", +built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing", +literal:"true false null"},a={className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"@" +},s={className:"subst",begin:/\$\{/,end:/\}/,contains:[e.C_NUMBER_MODE]},t={ +className:"variable",begin:"\\$"+e.UNDERSCORE_IDENT_RE},r={className:"string", +variants:[{begin:'"""',end:'"""(?=[^"])',contains:[t,s]},{begin:"'",end:"'", +illegal:/\n/,contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/, +contains:[e.BACKSLASH_ESCAPE,t,s]}]};s.contains.push(r);const l={ +className:"meta", +begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+e.UNDERSCORE_IDENT_RE+")?" +},c={className:"meta",begin:"@"+e.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/, +end:/\)/,contains:[e.inherit(r,{className:"string"}),"self"]}] +},o=i,b=e.COMMENT("/\\*","\\*/",{contains:[e.C_BLOCK_COMMENT_MODE]}),E={ +variants:[{className:"type",begin:e.UNDERSCORE_IDENT_RE},{begin:/\(/,end:/\)/, +contains:[]}]},d=E;return d.variants[1].contains=[E],E.variants[1].contains=[d], +{name:"Kotlin",aliases:["kt","kts"],keywords:n, +contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag", +begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,b,{className:"keyword", +begin:/\b(break|continue|return|this)\b/,starts:{contains:[{className:"symbol", +begin:/@\w+/}]}},a,l,c,{className:"function",beginKeywords:"fun",end:"[(]|$", +returnBegin:!0,excludeEnd:!0,keywords:n,relevance:5,contains:[{ +begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0, +contains:[e.UNDERSCORE_TITLE_MODE]},{className:"type",begin://, +keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/, +endsParent:!0,keywords:n,relevance:0,contains:[{begin:/:/,end:/[=,\/]/, +endsWithParent:!0,contains:[E,e.C_LINE_COMMENT_MODE,b],relevance:0 +},e.C_LINE_COMMENT_MODE,b,l,c,r,e.C_NUMBER_MODE]},b]},{ +begin:[/class|interface|trait/,/\s+/,e.UNDERSCORE_IDENT_RE],beginScope:{ +3:"title.class"},keywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0, +illegal:"extends implements",contains:[{ +beginKeywords:"public protected internal private constructor" +},e.UNDERSCORE_TITLE_MODE,{className:"type",begin://,excludeBegin:!0, +excludeEnd:!0,relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,){\s]|$/, +excludeBegin:!0,returnEnd:!0},l,c]},r,{className:"meta",begin:"^#!/usr/bin/env", +end:"$",illegal:"\n"},o]}}})();hljs.registerLanguage("kotlin",e)})();/*! `less` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict" +;const e=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video","defs","g","marker","mask","pattern","svg","switch","symbol","feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feFlood","feGaussianBlur","feImage","feMerge","feMorphology","feOffset","feSpecularLighting","feTile","feTurbulence","linearGradient","radialGradient","stop","circle","ellipse","image","line","path","polygon","polyline","rect","text","use","textPath","tspan","foreignObject","clipPath"],r=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"].sort().reverse(),t=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"].sort().reverse(),i=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"].sort().reverse(),o=["align-content","align-items","align-self","alignment-baseline","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","baseline-shift","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","cx","cy","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","dominant-baseline","empty-cells","enable-background","fill","fill-opacity","fill-rule","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","flood-color","flood-opacity","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-horizontal","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","kerning","justify-content","left","letter-spacing","lighting-color","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","marker","marker-end","marker-mid","marker-start","mask","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","r","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","shape-rendering","stop-color","stop-opacity","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","speak","speak-as","src","tab-size","table-layout","text-anchor","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vector-effect","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","x","y","z-index"].sort().reverse(),n=t.concat(i).sort().reverse() +;return a=>{const l=(e=>({IMPORTANT:{scope:"meta",begin:"!important"}, +BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{scope:"number", +begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/},FUNCTION_DISPATCH:{ +className:"built_in",begin:/[\w-]+(?=\()/},ATTRIBUTE_SELECTOR_MODE:{ +scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", +contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ +scope:"number", +begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", +relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z_][A-Za-z0-9_-]*/} +}))(a),s=n,d="[\\w-]+",c="("+d+"|@\\{"+d+"\\})",g=[],b=[],m=e=>({ +className:"string",begin:"~?"+e+".*?"+e}),p=(e,r,t)=>({className:e,begin:r, +relevance:t}),f={$pattern:/[a-z-]+/,keyword:"and or not only", +attribute:r.join(" ")},u={begin:"\\(",end:"\\)",contains:b,keywords:f, +relevance:0} +;b.push(a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,m("'"),m('"'),l.CSS_NUMBER_MODE,{ +begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]", +excludeEnd:!0} +},l.HEXCOLOR,u,p("variable","@@?"+d,10),p("variable","@\\{"+d+"\\}"),p("built_in","~?`[^`]*?`"),{ +className:"attribute",begin:d+"\\s*:",end:":",returnBegin:!0,excludeEnd:!0 +},l.IMPORTANT,{beginKeywords:"and not"},l.FUNCTION_DISPATCH);const h=b.concat({ +begin:/\{/,end:/\}/,contains:g}),k={beginKeywords:"when",endsWithParent:!0, +contains:[{beginKeywords:"and not"}].concat(b)},v={begin:c+"\\s*:", +returnBegin:!0,end:/[;}]/,relevance:0,contains:[{begin:/-(webkit|moz|ms|o)-/ +},l.CSS_VARIABLE,{className:"attribute",begin:"\\b("+o.join("|")+")\\b", +end:/(?=:)/,starts:{endsWithParent:!0,illegal:"[<=$]",relevance:0,contains:b}}] +},y={className:"keyword", +begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b", +starts:{end:"[;{}]",keywords:f,returnEnd:!0,contains:b,relevance:0}},w={ +className:"variable",variants:[{begin:"@"+d+"\\s*:",relevance:15},{begin:"@"+d +}],starts:{end:"[;}]",returnEnd:!0,contains:h}},x={variants:[{ +begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:c,end:/\{/}],returnBegin:!0, +returnEnd:!0,illegal:"[<='$\"]",relevance:0, +contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,k,p("keyword","all\\b"),p("variable","@\\{"+d+"\\}"),{ +begin:"\\b("+e.join("|")+")\\b",className:"selector-tag" +},l.CSS_NUMBER_MODE,p("selector-tag",c,0),p("selector-id","#"+c),p("selector-class","\\."+c,0),p("selector-tag","&",0),l.ATTRIBUTE_SELECTOR_MODE,{ +className:"selector-pseudo",begin:":("+t.join("|")+")"},{ +className:"selector-pseudo",begin:":(:)?("+i.join("|")+")"},{begin:/\(/, +end:/\)/,relevance:0,contains:h},{begin:"!important"},l.FUNCTION_DISPATCH]},_={ +begin:d+":(:)?"+`(${s.join("|")})`,returnBegin:!0,contains:[x]} +;return g.push(a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,y,w,_,v,x,k,l.FUNCTION_DISPATCH), +{name:"Less",case_insensitive:!0,illegal:"[=>'/<($\"]",contains:g}}})() +;hljs.registerLanguage("less",e)})();/*! `lua` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const t="\\[=*\\[",a="\\]=*\\]",n={ +begin:t,end:a,contains:["self"] +},o=[e.COMMENT("--(?!"+t+")","$"),e.COMMENT("--"+t,a,{contains:[n],relevance:10 +})];return{name:"Lua",keywords:{$pattern:e.UNDERSCORE_IDENT_RE, +literal:"true false nil", +keyword:"and break do else elseif end for goto if in local not or repeat return then until while", +built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove" +},contains:o.concat([{className:"function",beginKeywords:"function",end:"\\)", +contains:[e.inherit(e.TITLE_MODE,{ +begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params", +begin:"\\(",endsWithParent:!0,contains:o}].concat(o) +},e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"string", +begin:t,end:a,contains:[n],relevance:5}])}}})();hljs.registerLanguage("lua",e) +})();/*! `makefile` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const i={className:"variable", +variants:[{begin:"\\$\\("+e.UNDERSCORE_IDENT_RE+"\\)", +contains:[e.BACKSLASH_ESCAPE]},{begin:/\$[@%{var e=(()=>{"use strict";return e=>{const n={begin:/<\/?[A-Za-z_]/, +end:">",subLanguage:"xml",relevance:0},a={variants:[{begin:/\[.+?\]\[.*?\]/, +relevance:0},{ +begin:/\[.+?\]\(((data|javascript|mailto):|(?:http|ftp)s?:\/\/).*?\)/, +relevance:2},{ +begin:e.regex.concat(/\[.+?\]\(/,/[A-Za-z][A-Za-z0-9+.-]*/,/:\/\/.*?\)/), +relevance:2},{begin:/\[.+?\]\([./?&#].*?\)/,relevance:1},{ +begin:/\[.*?\]\(.*?\)/,relevance:0}],returnBegin:!0,contains:[{match:/\[(?=\])/ +},{className:"string",relevance:0,begin:"\\[",end:"\\]",excludeBegin:!0, +returnEnd:!0},{className:"link",relevance:0,begin:"\\]\\(",end:"\\)", +excludeBegin:!0,excludeEnd:!0},{className:"symbol",relevance:0,begin:"\\]\\[", +end:"\\]",excludeBegin:!0,excludeEnd:!0}]},i={className:"strong",contains:[], +variants:[{begin:/_{2}(?!\s)/,end:/_{2}/},{begin:/\*{2}(?!\s)/,end:/\*{2}/}] +},s={className:"emphasis",contains:[],variants:[{begin:/\*(?![*\s])/,end:/\*/},{ +begin:/_(?![_\s])/,end:/_/,relevance:0}]},c=e.inherit(i,{contains:[] +}),t=e.inherit(s,{contains:[]});i.contains.push(t),s.contains.push(c) +;let g=[n,a];return[i,s,c,t].forEach((e=>{e.contains=e.contains.concat(g) +})),g=g.concat(i,s),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{ +className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:g},{ +begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n", +contains:g}]}]},n,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)", +end:"\\s+",excludeEnd:!0},i,s,{className:"quote",begin:"^>\\s+",contains:g, +end:"$"},{className:"code",variants:[{begin:"(`{3,})[^`](.|\\n)*?\\1`*[ ]*"},{ +begin:"(~{3,})[^~](.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{ +begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))", +contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{ +begin:"^[-\\*]{3,}",end:"$"},a,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{ +className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{ +className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}}})() +;hljs.registerLanguage("markdown",e)})();/*! `objectivec` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const n=/[a-zA-Z@][a-zA-Z0-9_]*/,_={ +$pattern:n,keyword:["@interface","@class","@protocol","@implementation"]} +;return{name:"Objective-C", +aliases:["mm","objc","obj-c","obj-c++","objective-c++"],keywords:{ +"variable.language":["this","super"],$pattern:n, +keyword:["while","export","sizeof","typedef","const","struct","for","union","volatile","static","mutable","if","do","return","goto","enum","else","break","extern","asm","case","default","register","explicit","typename","switch","continue","inline","readonly","assign","readwrite","self","@synchronized","id","typeof","nonatomic","IBOutlet","IBAction","strong","weak","copy","in","out","inout","bycopy","byref","oneway","__strong","__weak","__block","__autoreleasing","@private","@protected","@public","@try","@property","@end","@throw","@catch","@finally","@autoreleasepool","@synthesize","@dynamic","@selector","@optional","@required","@encode","@package","@import","@defs","@compatibility_alias","__bridge","__bridge_transfer","__bridge_retained","__bridge_retain","__covariant","__contravariant","__kindof","_Nonnull","_Nullable","_Null_unspecified","__FUNCTION__","__PRETTY_FUNCTION__","__attribute__","getter","setter","retain","unsafe_unretained","nonnull","nullable","null_unspecified","null_resettable","class","instancetype","NS_DESIGNATED_INITIALIZER","NS_UNAVAILABLE","NS_REQUIRES_SUPER","NS_RETURNS_INNER_POINTER","NS_INLINE","NS_AVAILABLE","NS_DEPRECATED","NS_ENUM","NS_OPTIONS","NS_SWIFT_UNAVAILABLE","NS_ASSUME_NONNULL_BEGIN","NS_ASSUME_NONNULL_END","NS_REFINED_FOR_SWIFT","NS_SWIFT_NAME","NS_SWIFT_NOTHROW","NS_DURING","NS_HANDLER","NS_ENDHANDLER","NS_VALUERETURN","NS_VOIDRETURN"], +literal:["false","true","FALSE","TRUE","nil","YES","NO","NULL"], +built_in:["dispatch_once_t","dispatch_queue_t","dispatch_sync","dispatch_async","dispatch_once"], +type:["int","float","char","unsigned","signed","short","long","double","wchar_t","unichar","void","bool","BOOL","id|0","_Bool"] +},illegal:"/,end:/$/,illegal:"\\n" +},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"class", +begin:"("+_.keyword.join("|")+")\\b",end:/(\{|$)/,excludeEnd:!0,keywords:_, +contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"\\."+e.UNDERSCORE_IDENT_RE, +relevance:0}]}}})();hljs.registerLanguage("objectivec",e)})();/*! `perl` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const n=e.regex,t=/[dualxmsipngr]{0,12}/,s={$pattern:/[\w.]+/, +keyword:"abs accept alarm and atan2 bind binmode bless break caller chdir chmod chomp chop chown chr chroot class close closedir connect continue cos crypt dbmclose dbmopen defined delete die do dump each else elsif endgrent endhostent endnetent endprotoent endpwent endservent eof eval exec exists exit exp fcntl field fileno flock for foreach fork format formline getc getgrent getgrgid getgrnam gethostbyaddr gethostbyname gethostent getlogin getnetbyaddr getnetbyname getnetent getpeername getpgrp getpriority getprotobyname getprotobynumber getprotoent getpwent getpwnam getpwuid getservbyname getservbyport getservent getsockname getsockopt given glob gmtime goto grep gt hex if index int ioctl join keys kill last lc lcfirst length link listen local localtime log lstat lt ma map method mkdir msgctl msgget msgrcv msgsnd my ne next no not oct open opendir or ord our pack package pipe pop pos print printf prototype push q|0 qq quotemeta qw qx rand read readdir readline readlink readpipe recv redo ref rename require reset return reverse rewinddir rindex rmdir say scalar seek seekdir select semctl semget semop send setgrent sethostent setnetent setpgrp setpriority setprotoent setpwent setservent setsockopt shift shmctl shmget shmread shmwrite shutdown sin sleep socket socketpair sort splice split sprintf sqrt srand stat state study sub substr symlink syscall sysopen sysread sysseek system syswrite tell telldir tie tied time times tr truncate uc ucfirst umask undef unless unlink unpack unshift untie until use utime values vec wait waitpid wantarray warn when while write x|0 xor y|0" +},r={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:s},a={begin:/->\{/, +end:/\}/},i={scope:"attr",match:/\s+:\s*\w+(\s*\(.*?\))?/},c={scope:"variable", +variants:[{begin:/\$\d/},{ +begin:n.concat(/[$%@](\^\w\b|#\w+(::\w+)*|\{\w+\}|\w+(::\w*)*)/,"(?![A-Za-z])(?![@$%])") +},{begin:/[$%@][^\s\w{=]|\$=/,relevance:0}],contains:[i]},o={className:"number", +variants:[{match:/0?\.[0-9][0-9_]+\b/},{ +match:/\bv?(0|[1-9][0-9_]*(\.[0-9_]+)?|[1-9][0-9_]*)\b/},{ +match:/\b0[0-7][0-7_]*\b/},{match:/\b0x[0-9a-fA-F][0-9a-fA-F_]*\b/},{ +match:/\b0b[0-1][0-1_]*\b/}],relevance:0 +},l=[e.BACKSLASH_ESCAPE,r,c],g=[/!/,/\//,/\|/,/\?/,/'/,/"/,/#/],d=(e,s,r="\\1")=>{ +const a="\\1"===r?r:n.concat(r,s) +;return n.concat(n.concat("(?:",e,")"),s,/(?:\\.|[^\\\/])*?/,a,/(?:\\.|[^\\\/])*?/,r,t) +},m=(e,s,r)=>n.concat(n.concat("(?:",e,")"),s,/(?:\\.|[^\\\/])*?/,r,t),p=[c,e.HASH_COMMENT_MODE,e.COMMENT(/^=\w/,/=cut/,{ +endsWithParent:!0}),a,{className:"string",contains:l,variants:[{ +begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[", +end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{ +begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*<",end:">", +relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'", +contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`", +contains:[e.BACKSLASH_ESCAPE]},{begin:/\{\w+\}/,relevance:0},{ +begin:"-?\\w+\\s*=>",relevance:0}]},o,{ +begin:"(\\/\\/|"+e.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*", +keywords:"split return print reverse grep",relevance:0, +contains:[e.HASH_COMMENT_MODE,{className:"regexp",variants:[{ +begin:d("s|tr|y",n.either(...g,{capture:!0}))},{begin:d("s|tr|y","\\(","\\)")},{ +begin:d("s|tr|y","\\[","\\]")},{begin:d("s|tr|y","\\{","\\}")}],relevance:2},{ +className:"regexp",variants:[{begin:/(m|qr)\/\//,relevance:0},{ +begin:m("(?:m|qr)?",/\//,/\//)},{begin:m("m|qr",n.either(...g,{capture:!0 +}),/\1/)},{begin:m("m|qr",/\(/,/\)/)},{begin:m("m|qr",/\[/,/\]/)},{ +begin:m("m|qr",/\{/,/\}/)}]}]},{className:"function",beginKeywords:"sub method", +end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[e.TITLE_MODE,i] +},{className:"class",beginKeywords:"class",end:"[;{]",excludeEnd:!0,relevance:5, +contains:[e.TITLE_MODE,i,o]},{begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$", +end:"^__END__$",subLanguage:"mojolicious",contains:[{begin:"^@@.*",end:"$", +className:"comment"}]}];return r.contains=p,a.contains=p,{name:"Perl", +aliases:["pl","pm"],keywords:s,contains:p}}})();hljs.registerLanguage("perl",e) +})();/*! `php` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const t=e.regex,a=/(?![A-Za-z0-9])(?![$])/,r=t.concat(/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/,a),n=t.concat(/(\\?[A-Z][a-z0-9_\x7f-\xff]+|\\?[A-Z]+(?=[A-Z][a-z0-9_\x7f-\xff])){1,}/,a),o={ +scope:"variable",match:"\\$+"+r},c={scope:"subst",variants:[{begin:/\$\w+/},{ +begin:/\{\$/,end:/\}/}]},i=e.inherit(e.APOS_STRING_MODE,{illegal:null +}),s="[ \t\n]",l={scope:"string",variants:[e.inherit(e.QUOTE_STRING_MODE,{ +illegal:null,contains:e.QUOTE_STRING_MODE.contains.concat(c)}),i,{ +begin:/<<<[ \t]*(?:(\w+)|"(\w+)")\n/,end:/[ \t]*(\w+)\b/, +contains:e.QUOTE_STRING_MODE.contains.concat(c),"on:begin":(e,t)=>{ +t.data._beginMatch=e[1]||e[2]},"on:end":(e,t)=>{ +t.data._beginMatch!==e[1]&&t.ignoreMatch()}},e.END_SAME_AS_BEGIN({ +begin:/<<<[ \t]*'(\w+)'\n/,end:/[ \t]*(\w+)\b/})]},d={scope:"number",variants:[{ +begin:"\\b0[bB][01]+(?:_[01]+)*\\b"},{begin:"\\b0[oO][0-7]+(?:_[0-7]+)*\\b"},{ +begin:"\\b0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*\\b"},{ +begin:"(?:\\b\\d+(?:_\\d+)*(\\.(?:\\d+(?:_\\d+)*))?|\\B\\.\\d+)(?:[eE][+-]?\\d+)?" +}],relevance:0 +},_=["false","null","true"],p=["__CLASS__","__DIR__","__FILE__","__FUNCTION__","__COMPILER_HALT_OFFSET__","__LINE__","__METHOD__","__NAMESPACE__","__TRAIT__","die","echo","exit","include","include_once","print","require","require_once","array","abstract","and","as","binary","bool","boolean","break","callable","case","catch","class","clone","const","continue","declare","default","do","double","else","elseif","empty","enddeclare","endfor","endforeach","endif","endswitch","endwhile","enum","eval","extends","final","finally","float","for","foreach","from","global","goto","if","implements","instanceof","insteadof","int","integer","interface","isset","iterable","list","match|0","mixed","new","never","object","or","private","protected","public","readonly","real","return","string","switch","throw","trait","try","unset","use","var","void","while","xor","yield"],b=["Error|0","AppendIterator","ArgumentCountError","ArithmeticError","ArrayIterator","ArrayObject","AssertionError","BadFunctionCallException","BadMethodCallException","CachingIterator","CallbackFilterIterator","CompileError","Countable","DirectoryIterator","DivisionByZeroError","DomainException","EmptyIterator","ErrorException","Exception","FilesystemIterator","FilterIterator","GlobIterator","InfiniteIterator","InvalidArgumentException","IteratorIterator","LengthException","LimitIterator","LogicException","MultipleIterator","NoRewindIterator","OutOfBoundsException","OutOfRangeException","OuterIterator","OverflowException","ParentIterator","ParseError","RangeException","RecursiveArrayIterator","RecursiveCachingIterator","RecursiveCallbackFilterIterator","RecursiveDirectoryIterator","RecursiveFilterIterator","RecursiveIterator","RecursiveIteratorIterator","RecursiveRegexIterator","RecursiveTreeIterator","RegexIterator","RuntimeException","SeekableIterator","SplDoublyLinkedList","SplFileInfo","SplFileObject","SplFixedArray","SplHeap","SplMaxHeap","SplMinHeap","SplObjectStorage","SplObserver","SplPriorityQueue","SplQueue","SplStack","SplSubject","SplTempFileObject","TypeError","UnderflowException","UnexpectedValueException","UnhandledMatchError","ArrayAccess","BackedEnum","Closure","Fiber","Generator","Iterator","IteratorAggregate","Serializable","Stringable","Throwable","Traversable","UnitEnum","WeakReference","WeakMap","Directory","__PHP_Incomplete_Class","parent","php_user_filter","self","static","stdClass"],E={ +keyword:p,literal:(e=>{const t=[];return e.forEach((e=>{ +t.push(e),e.toLowerCase()===e?t.push(e.toUpperCase()):t.push(e.toLowerCase()) +})),t})(_),built_in:b},u=e=>e.map((e=>e.replace(/\|\d+$/,""))),g={variants:[{ +match:[/new/,t.concat(s,"+"),t.concat("(?!",u(b).join("\\b|"),"\\b)"),n],scope:{ +1:"keyword",4:"title.class"}}]},h=t.concat(r,"\\b(?!\\()"),m={variants:[{ +match:[t.concat(/::/,t.lookahead(/(?!class\b)/)),h],scope:{2:"variable.constant" +}},{match:[/::/,/class/],scope:{2:"variable.language"}},{ +match:[n,t.concat(/::/,t.lookahead(/(?!class\b)/)),h],scope:{1:"title.class", +3:"variable.constant"}},{match:[n,t.concat("::",t.lookahead(/(?!class\b)/))], +scope:{1:"title.class"}},{match:[n,/::/,/class/],scope:{1:"title.class", +3:"variable.language"}}]},I={scope:"attr", +match:t.concat(r,t.lookahead(":"),t.lookahead(/(?!::)/))},f={relevance:0, +begin:/\(/,end:/\)/,keywords:E,contains:[I,o,m,e.C_BLOCK_COMMENT_MODE,l,d,g] +},O={relevance:0, +match:[/\b/,t.concat("(?!fn\\b|function\\b|",u(p).join("\\b|"),"|",u(b).join("\\b|"),"\\b)"),r,t.concat(s,"*"),t.lookahead(/(?=\()/)], +scope:{3:"title.function.invoke"},contains:[f]};f.contains.push(O) +;const v=[I,m,e.C_BLOCK_COMMENT_MODE,l,d,g];return{case_insensitive:!1, +keywords:E,contains:[{begin:t.concat(/#\[\s*/,n),beginScope:"meta",end:/]/, +endScope:"meta",keywords:{literal:_,keyword:["new","array"]},contains:[{ +begin:/\[/,end:/]/,keywords:{literal:_,keyword:["new","array"]}, +contains:["self",...v]},...v,{scope:"meta",match:n}] +},e.HASH_COMMENT_MODE,e.COMMENT("//","$"),e.COMMENT("/\\*","\\*/",{contains:[{ +scope:"doctag",match:"@[A-Za-z]+"}]}),{match:/__halt_compiler\(\);/, +keywords:"__halt_compiler",starts:{scope:"comment",end:e.MATCH_NOTHING_RE, +contains:[{match:/\?>/,scope:"meta",endsParent:!0}]}},{scope:"meta",variants:[{ +begin:/<\?php/,relevance:10},{begin:/<\?=/},{begin:/<\?/,relevance:.1},{ +begin:/\?>/}]},{scope:"variable.language",match:/\$this\b/},o,O,m,{ +match:[/const/,/\s/,r],scope:{1:"keyword",3:"variable.constant"}},g,{ +scope:"function",relevance:0,beginKeywords:"fn function",end:/[;{]/, +excludeEnd:!0,illegal:"[$%\\[]",contains:[{beginKeywords:"use" +},e.UNDERSCORE_TITLE_MODE,{begin:"=>",endsParent:!0},{scope:"params", +begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0,keywords:E, +contains:["self",o,m,e.C_BLOCK_COMMENT_MODE,l,d]}]},{scope:"class",variants:[{ +beginKeywords:"enum",illegal:/[($"]/},{beginKeywords:"class interface trait", +illegal:/[:($"]/}],relevance:0,end:/\{/,excludeEnd:!0,contains:[{ +beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{ +beginKeywords:"namespace",relevance:0,end:";",illegal:/[.']/, +contains:[e.inherit(e.UNDERSCORE_TITLE_MODE,{scope:"title.class"})]},{ +beginKeywords:"use",relevance:0,end:";",contains:[{ +match:/\b(as|const|function)\b/,scope:"keyword"},e.UNDERSCORE_TITLE_MODE]},l,d]} +}})();hljs.registerLanguage("php",e)})();/*! `php-template` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var n=(()=>{"use strict";return n=>({name:"PHP template", +subLanguage:"xml",contains:[{begin:/<\?(php|=)?/,end:/\?>/,subLanguage:"php", +contains:[{begin:"/\\*",end:"\\*/",skip:!0},{begin:'b"',end:'"',skip:!0},{ +begin:"b'",end:"'",skip:!0},n.inherit(n.APOS_STRING_MODE,{illegal:null, +className:null,contains:null,skip:!0}),n.inherit(n.QUOTE_STRING_MODE,{ +illegal:null,className:null,contains:null,skip:!0})]}]})})() +;hljs.registerLanguage("php-template",n)})();/*! `plaintext` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var t=(()=>{"use strict";return t=>({name:"Plain text", +aliases:["text","txt"],disableAutodetect:!0})})() +;hljs.registerLanguage("plaintext",t)})();/*! `python` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const n=e.regex,a=/[\p{XID_Start}_]\p{XID_Continue}*/u,s=["and","as","assert","async","await","break","case","class","continue","def","del","elif","else","except","finally","for","from","global","if","import","in","is","lambda","match","nonlocal|10","not","or","pass","raise","return","try","while","with","yield"],i={ +$pattern:/[A-Za-z]\w+|__\w+__/,keyword:s, +built_in:["__import__","abs","all","any","ascii","bin","bool","breakpoint","bytearray","bytes","callable","chr","classmethod","compile","complex","delattr","dict","dir","divmod","enumerate","eval","exec","filter","float","format","frozenset","getattr","globals","hasattr","hash","help","hex","id","input","int","isinstance","issubclass","iter","len","list","locals","map","max","memoryview","min","next","object","oct","open","ord","pow","print","property","range","repr","reversed","round","set","setattr","slice","sorted","staticmethod","str","sum","super","tuple","type","vars","zip"], +literal:["__debug__","Ellipsis","False","None","NotImplemented","True"], +type:["Any","Callable","Coroutine","Dict","List","Literal","Generic","Optional","Sequence","Set","Tuple","Type","Union"] +},t={className:"meta",begin:/^(>>>|\.\.\.) /},r={className:"subst",begin:/\{/, +end:/\}/,keywords:i,illegal:/#/},l={begin:/\{\{/,relevance:0},b={ +className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{ +begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?'''/,end:/'''/, +contains:[e.BACKSLASH_ESCAPE,t],relevance:10},{ +begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?"""/,end:/"""/, +contains:[e.BACKSLASH_ESCAPE,t],relevance:10},{ +begin:/([fF][rR]|[rR][fF]|[fF])'''/,end:/'''/, +contains:[e.BACKSLASH_ESCAPE,t,l,r]},{begin:/([fF][rR]|[rR][fF]|[fF])"""/, +end:/"""/,contains:[e.BACKSLASH_ESCAPE,t,l,r]},{begin:/([uU]|[rR])'/,end:/'/, +relevance:10},{begin:/([uU]|[rR])"/,end:/"/,relevance:10},{ +begin:/([bB]|[bB][rR]|[rR][bB])'/,end:/'/},{begin:/([bB]|[bB][rR]|[rR][bB])"/, +end:/"/},{begin:/([fF][rR]|[rR][fF]|[fF])'/,end:/'/, +contains:[e.BACKSLASH_ESCAPE,l,r]},{begin:/([fF][rR]|[rR][fF]|[fF])"/,end:/"/, +contains:[e.BACKSLASH_ESCAPE,l,r]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE] +},o="[0-9](_?[0-9])*",c=`(\\b(${o}))?\\.(${o})|\\b(${o})\\.`,d="\\b|"+s.join("|"),g={ +className:"number",relevance:0,variants:[{ +begin:`(\\b(${o})|(${c}))[eE][+-]?(${o})[jJ]?(?=${d})`},{begin:`(${c})[jJ]?`},{ +begin:`\\b([1-9](_?[0-9])*|0+(_?0)*)[lLjJ]?(?=${d})`},{ +begin:`\\b0[bB](_?[01])+[lL]?(?=${d})`},{begin:`\\b0[oO](_?[0-7])+[lL]?(?=${d})` +},{begin:`\\b0[xX](_?[0-9a-fA-F])+[lL]?(?=${d})`},{begin:`\\b(${o})[jJ](?=${d})` +}]},p={className:"comment",begin:n.lookahead(/# type:/),end:/$/,keywords:i, +contains:[{begin:/# type:/},{begin:/#/,end:/\b\B/,endsWithParent:!0}]},m={ +className:"params",variants:[{className:"",begin:/\(\s*\)/,skip:!0},{begin:/\(/, +end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:i, +contains:["self",t,g,b,e.HASH_COMMENT_MODE]}]};return r.contains=[b,g,t],{ +name:"Python",aliases:["py","gyp","ipython"],unicodeRegex:!0,keywords:i, +illegal:/(<\/|\?)|=>/,contains:[t,g,{begin:/\bself\b/},{beginKeywords:"if", +relevance:0},{match:/\bor\b/,scope:"keyword"},b,p,e.HASH_COMMENT_MODE,{ +match:[/\bdef/,/\s+/,a],scope:{1:"keyword",3:"title.function"},contains:[m]},{ +variants:[{match:[/\bclass/,/\s+/,a,/\s*/,/\(\s*/,a,/\s*\)/]},{ +match:[/\bclass/,/\s+/,a]}],scope:{1:"keyword",3:"title.class", +6:"title.class.inherited"}},{className:"meta",begin:/^[\t ]*@/,end:/(?=#)|$/, +contains:[g,m,b]}]}}})();hljs.registerLanguage("python",e)})();/*! `python-repl` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var a=(()=>{"use strict";return a=>({aliases:["pycon"],contains:[{ +className:"meta.prompt",starts:{end:/ |$/,starts:{end:"$",subLanguage:"python"} +},variants:[{begin:/^>>>(?=[ ]|$)/},{begin:/^\.\.\.(?=[ ]|$)/}]}]})})() +;hljs.registerLanguage("python-repl",a)})();/*! `r` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const a=e.regex,n=/(?:(?:[a-zA-Z]|\.[._a-zA-Z])[._a-zA-Z0-9]*)|\.(?!\d)/,i=a.either(/0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*[pP][+-]?\d+i?/,/0[xX][0-9a-fA-F]+(?:[pP][+-]?\d+)?[Li]?/,/(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?[Li]?/),s=/[=!<>:]=|\|\||&&|:::?|<-|<<-|->>|->|\|>|[-+*\/?!$&|:<=>@^~]|\*\*/,t=a.either(/[()]/,/[{}]/,/\[\[/,/[[\]]/,/\\/,/,/) +;return{name:"R",keywords:{$pattern:n, +keyword:"function if in break next repeat else for while", +literal:"NULL NA TRUE FALSE Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10", +built_in:"LETTERS letters month.abb month.name pi T F abs acos acosh all any anyNA Arg as.call as.character as.complex as.double as.environment as.integer as.logical as.null.default as.numeric as.raw asin asinh atan atanh attr attributes baseenv browser c call ceiling class Conj cos cosh cospi cummax cummin cumprod cumsum digamma dim dimnames emptyenv exp expression floor forceAndCall gamma gc.time globalenv Im interactive invisible is.array is.atomic is.call is.character is.complex is.double is.environment is.expression is.finite is.function is.infinite is.integer is.language is.list is.logical is.matrix is.na is.name is.nan is.null is.numeric is.object is.pairlist is.raw is.recursive is.single is.symbol lazyLoadDBfetch length lgamma list log max min missing Mod names nargs nzchar oldClass on.exit pos.to.env proc.time prod quote range Re rep retracemem return round seq_along seq_len seq.int sign signif sin sinh sinpi sqrt standardGeneric substitute sum switch tan tanh tanpi tracemem trigamma trunc unclass untracemem UseMethod xtfrm" +},contains:[e.COMMENT(/#'/,/$/,{contains:[{scope:"doctag",match:/@examples/, +starts:{end:a.lookahead(a.either(/\n^#'\s*(?=@[a-zA-Z]+)/,/\n^(?!#')/)), +endsParent:!0}},{scope:"doctag",begin:"@param",end:/$/,contains:[{ +scope:"variable",variants:[{match:n},{match:/`(?:\\.|[^`\\])+`/}],endsParent:!0 +}]},{scope:"doctag",match:/@[a-zA-Z]+/},{scope:"keyword",match:/\\[a-zA-Z]+/}] +}),e.HASH_COMMENT_MODE,{scope:"string",contains:[e.BACKSLASH_ESCAPE], +variants:[e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\(/,end:/\)(-*)"/ +}),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\{/,end:/\}(-*)"/ +}),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\[/,end:/\](-*)"/ +}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\(/,end:/\)(-*)'/ +}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\{/,end:/\}(-*)'/ +}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\[/,end:/\](-*)'/}),{begin:'"',end:'"', +relevance:0},{begin:"'",end:"'",relevance:0}]},{relevance:0,variants:[{scope:{ +1:"operator",2:"number"},match:[s,i]},{scope:{1:"operator",2:"number"}, +match:[/%[^%]*%/,i]},{scope:{1:"punctuation",2:"number"},match:[t,i]},{scope:{ +2:"number"},match:[/[^a-zA-Z0-9._]|^/,i]}]},{scope:{3:"operator"}, +match:[n,/\s+/,/<-/,/\s+/]},{scope:"operator",relevance:0,variants:[{match:s},{ +match:/%[^%]*%/}]},{scope:"punctuation",relevance:0,match:t},{begin:"`",end:"`", +contains:[{begin:/\\./}]}]}}})();hljs.registerLanguage("r",e)})();/*! `ruby` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const n=e.regex,a="([a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?)",s=n.either(/\b([A-Z]+[a-z0-9]+)+/,/\b([A-Z]+[a-z0-9]+)+[A-Z]+/),i=n.concat(s,/(::\w+)*/),t={ +"variable.constant":["__FILE__","__LINE__","__ENCODING__"], +"variable.language":["self","super"], +keyword:["alias","and","begin","BEGIN","break","case","class","defined","do","else","elsif","end","END","ensure","for","if","in","module","next","not","or","redo","require","rescue","retry","return","then","undef","unless","until","when","while","yield","include","extend","prepend","public","private","protected","raise","throw"], +built_in:["proc","lambda","attr_accessor","attr_reader","attr_writer","define_method","private_constant","module_function"], +literal:["true","false","nil"]},c={className:"doctag",begin:"@[A-Za-z]+"},r={ +begin:"#<",end:">"},b=[e.COMMENT("#","$",{contains:[c] +}),e.COMMENT("^=begin","^=end",{contains:[c],relevance:10 +}),e.COMMENT("^__END__",e.MATCH_NOTHING_RE)],l={className:"subst",begin:/#\{/, +end:/\}/,keywords:t},d={className:"string",contains:[e.BACKSLASH_ESCAPE,l], +variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{ +begin:/%[qQwWx]?\(/,end:/\)/},{begin:/%[qQwWx]?\[/,end:/\]/},{ +begin:/%[qQwWx]?\{/,end:/\}/},{begin:/%[qQwWx]?/},{begin:/%[qQwWx]?\//, +end:/\//},{begin:/%[qQwWx]?%/,end:/%/},{begin:/%[qQwWx]?-/,end:/-/},{ +begin:/%[qQwWx]?\|/,end:/\|/},{begin:/\B\?(\\\d{1,3})/},{ +begin:/\B\?(\\x[A-Fa-f0-9]{1,2})/},{begin:/\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/},{ +begin:/\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/},{ +begin:/\B\?\\(c|C-)[\x20-\x7e]/},{begin:/\B\?\\?\S/},{ +begin:n.concat(/<<[-~]?'?/,n.lookahead(/(\w+)(?=\W)[^\n]*\n(?:[^\n]*\n)*?\s*\1\b/)), +contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/, +contains:[e.BACKSLASH_ESCAPE,l]})]}]},o="[0-9](_?[0-9])*",g={className:"number", +relevance:0,variants:[{ +begin:`\\b([1-9](_?[0-9])*|0)(\\.(${o}))?([eE][+-]?(${o})|r)?i?\\b`},{ +begin:"\\b0[dD][0-9](_?[0-9])*r?i?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*r?i?\\b" +},{begin:"\\b0[oO][0-7](_?[0-7])*r?i?\\b"},{ +begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*r?i?\\b"},{ +begin:"\\b0(_?[0-7])+r?i?\\b"}]},_={variants:[{match:/\(\)/},{ +className:"params",begin:/\(/,end:/(?=\))/,excludeBegin:!0,endsParent:!0, +keywords:t}]},u=[d,{variants:[{match:[/class\s+/,i,/\s+<\s+/,i]},{ +match:[/\b(class|module)\s+/,i]}],scope:{2:"title.class", +4:"title.class.inherited"},keywords:t},{match:[/(include|extend)\s+/,i],scope:{ +2:"title.class"},keywords:t},{relevance:0,match:[i,/\.new[. (]/],scope:{ +1:"title.class"}},{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, +className:"variable.constant"},{relevance:0,match:s,scope:"title.class"},{ +match:[/def/,/\s+/,a],scope:{1:"keyword",3:"title.function"},contains:[_]},{ +begin:e.IDENT_RE+"::"},{className:"symbol", +begin:e.UNDERSCORE_IDENT_RE+"(!|\\?)?:",relevance:0},{className:"symbol", +begin:":(?!\\s)",contains:[d,{begin:a}],relevance:0},g,{className:"variable", +begin:"(\\$\\W)|((\\$|@@?)(\\w+))(?=[^@$?])(?![A-Za-z])(?![@$?'])"},{ +className:"params",begin:/\|/,end:/\|/,excludeBegin:!0,excludeEnd:!0, +relevance:0,keywords:t},{begin:"("+e.RE_STARTERS_RE+"|unless)\\s*", +keywords:"unless",contains:[{className:"regexp",contains:[e.BACKSLASH_ESCAPE,l], +illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:/%r\{/,end:/\}[a-z]*/},{ +begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[", +end:"\\][a-z]*"}]}].concat(r,b),relevance:0}].concat(r,b) +;l.contains=u,_.contains=u;const m=[{begin:/^\s*=>/,starts:{end:"$",contains:u} +},{className:"meta.prompt", +begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+[>*]|(\\w+-)?\\d+\\.\\d+\\.\\d+(p\\d+)?[^\\d][^>]+>)(?=[ ])", +starts:{end:"$",keywords:t,contains:u}}];return b.unshift(r),{name:"Ruby", +aliases:["rb","gemspec","podspec","thor","irb"],keywords:t,illegal:/\/\*/, +contains:[e.SHEBANG({binary:"ruby"})].concat(m).concat(b).concat(u)}}})() +;hljs.registerLanguage("ruby",e)})();/*! `rust` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";return e=>{const t=e.regex,n={ +className:"title.function.invoke",relevance:0, +begin:t.concat(/\b/,/(?!let|for|while|if|else|match\b)/,e.IDENT_RE,t.lookahead(/\s*\(/)) +},a="([ui](8|16|32|64|128|size)|f(32|64))?",i=["drop ","Copy","Send","Sized","Sync","Drop","Fn","FnMut","FnOnce","ToOwned","Clone","Debug","PartialEq","PartialOrd","Eq","Ord","AsRef","AsMut","Into","From","Default","Iterator","Extend","IntoIterator","DoubleEndedIterator","ExactSizeIterator","SliceConcatExt","ToString","assert!","assert_eq!","bitflags!","bytes!","cfg!","col!","concat!","concat_idents!","debug_assert!","debug_assert_eq!","env!","eprintln!","panic!","file!","format!","format_args!","include_bytes!","include_str!","line!","local_data_key!","module_path!","option_env!","print!","println!","select!","stringify!","try!","unimplemented!","unreachable!","vec!","write!","writeln!","macro_rules!","assert_ne!","debug_assert_ne!"],s=["i8","i16","i32","i64","i128","isize","u8","u16","u32","u64","u128","usize","f32","f64","str","char","bool","Box","Option","Result","String","Vec"] +;return{name:"Rust",aliases:["rs"],keywords:{$pattern:e.IDENT_RE+"!?",type:s, +keyword:["abstract","as","async","await","become","box","break","const","continue","crate","do","dyn","else","enum","extern","false","final","fn","for","if","impl","in","let","loop","macro","match","mod","move","mut","override","priv","pub","ref","return","self","Self","static","struct","super","trait","true","try","type","typeof","unsafe","unsized","use","virtual","where","while","yield"], +literal:["true","false","Some","None","Ok","Err"],built_in:i},illegal:""},n]}}})() +;hljs.registerLanguage("rust",e)})();/*! `scss` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict" +;const e=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video","defs","g","marker","mask","pattern","svg","switch","symbol","feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feFlood","feGaussianBlur","feImage","feMerge","feMorphology","feOffset","feSpecularLighting","feTile","feTurbulence","linearGradient","radialGradient","stop","circle","ellipse","image","line","path","polygon","polyline","rect","text","use","textPath","tspan","foreignObject","clipPath"],r=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"].sort().reverse(),i=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"].sort().reverse(),t=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"].sort().reverse(),o=["align-content","align-items","align-self","alignment-baseline","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","baseline-shift","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","cx","cy","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","dominant-baseline","empty-cells","enable-background","fill","fill-opacity","fill-rule","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","flood-color","flood-opacity","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-horizontal","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","kerning","justify-content","left","letter-spacing","lighting-color","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","marker","marker-end","marker-mid","marker-start","mask","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","r","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","shape-rendering","stop-color","stop-opacity","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","speak","speak-as","src","tab-size","table-layout","text-anchor","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vector-effect","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","x","y","z-index"].sort().reverse() +;return n=>{const a=(e=>({IMPORTANT:{scope:"meta",begin:"!important"}, +BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{scope:"number", +begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/},FUNCTION_DISPATCH:{ +className:"built_in",begin:/[\w-]+(?=\()/},ATTRIBUTE_SELECTOR_MODE:{ +scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$", +contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{ +scope:"number", +begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?", +relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z_][A-Za-z0-9_-]*/} +}))(n),l=t,s=i,d="@[a-z-]+",c={className:"variable", +begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b",relevance:0};return{name:"SCSS", +case_insensitive:!0,illegal:"[=/|']", +contains:[n.C_LINE_COMMENT_MODE,n.C_BLOCK_COMMENT_MODE,a.CSS_NUMBER_MODE,{ +className:"selector-id",begin:"#[A-Za-z0-9_-]+",relevance:0},{ +className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0 +},a.ATTRIBUTE_SELECTOR_MODE,{className:"selector-tag", +begin:"\\b("+e.join("|")+")\\b",relevance:0},{className:"selector-pseudo", +begin:":("+s.join("|")+")"},{className:"selector-pseudo", +begin:":(:)?("+l.join("|")+")"},c,{begin:/\(/,end:/\)/, +contains:[a.CSS_NUMBER_MODE]},a.CSS_VARIABLE,{className:"attribute", +begin:"\\b("+o.join("|")+")\\b"},{ +begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b" +},{begin:/:/,end:/[;}{]/,relevance:0, +contains:[a.BLOCK_COMMENT,c,a.HEXCOLOR,a.CSS_NUMBER_MODE,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,a.IMPORTANT,a.FUNCTION_DISPATCH] +},{begin:"@(page|font-face)",keywords:{$pattern:d,keyword:"@page @font-face"}},{ +begin:"@",end:"[{;]",returnBegin:!0,keywords:{$pattern:/[a-z-]+/, +keyword:"and or not only",attribute:r.join(" ")},contains:[{begin:d, +className:"keyword"},{begin:/[a-z-]+(?=:)/,className:"attribute" +},c,n.QUOTE_STRING_MODE,n.APOS_STRING_MODE,a.HEXCOLOR,a.CSS_NUMBER_MODE] +},a.FUNCTION_DISPATCH]}}})();hljs.registerLanguage("scss",e)})();/*! `shell` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var s=(()=>{"use strict";return s=>({name:"Shell Session", +aliases:["console","shellsession"],contains:[{className:"meta.prompt", +begin:/^\s{0,3}[/~\w\d[\]()@-]*[>%$#][ ]?/,starts:{end:/[^\\](?=\s*$)/, +subLanguage:"bash"}}]})})();hljs.registerLanguage("shell",s)})();/*! `sql` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const r=e.regex,t=e.COMMENT("--","$"),n=["true","false","unknown"],a=["bigint","binary","blob","boolean","char","character","clob","date","dec","decfloat","decimal","float","int","integer","interval","nchar","nclob","national","numeric","real","row","smallint","time","timestamp","varchar","varying","varbinary"],i=["abs","acos","array_agg","asin","atan","avg","cast","ceil","ceiling","coalesce","corr","cos","cosh","count","covar_pop","covar_samp","cume_dist","dense_rank","deref","element","exp","extract","first_value","floor","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","last_value","lead","listagg","ln","log","log10","lower","max","min","mod","nth_value","ntile","nullif","percent_rank","percentile_cont","percentile_disc","position","position_regex","power","rank","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","row_number","sin","sinh","sqrt","stddev_pop","stddev_samp","substring","substring_regex","sum","tan","tanh","translate","translate_regex","treat","trim","trim_array","unnest","upper","value_of","var_pop","var_samp","width_bucket"],s=["create table","insert into","primary key","foreign key","not null","alter table","add constraint","grouping sets","on overflow","character set","respect nulls","ignore nulls","nulls first","nulls last","depth first","breadth first"],o=i,c=["abs","acos","all","allocate","alter","and","any","are","array","array_agg","array_max_cardinality","as","asensitive","asin","asymmetric","at","atan","atomic","authorization","avg","begin","begin_frame","begin_partition","between","bigint","binary","blob","boolean","both","by","call","called","cardinality","cascaded","case","cast","ceil","ceiling","char","char_length","character","character_length","check","classifier","clob","close","coalesce","collate","collect","column","commit","condition","connect","constraint","contains","convert","copy","corr","corresponding","cos","cosh","count","covar_pop","covar_samp","create","cross","cube","cume_dist","current","current_catalog","current_date","current_default_transform_group","current_path","current_role","current_row","current_schema","current_time","current_timestamp","current_path","current_role","current_transform_group_for_type","current_user","cursor","cycle","date","day","deallocate","dec","decimal","decfloat","declare","default","define","delete","dense_rank","deref","describe","deterministic","disconnect","distinct","double","drop","dynamic","each","element","else","empty","end","end_frame","end_partition","end-exec","equals","escape","every","except","exec","execute","exists","exp","external","extract","false","fetch","filter","first_value","float","floor","for","foreign","frame_row","free","from","full","function","fusion","get","global","grant","group","grouping","groups","having","hold","hour","identity","in","indicator","initial","inner","inout","insensitive","insert","int","integer","intersect","intersection","interval","into","is","join","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","language","large","last_value","lateral","lead","leading","left","like","like_regex","listagg","ln","local","localtime","localtimestamp","log","log10","lower","match","match_number","match_recognize","matches","max","member","merge","method","min","minute","mod","modifies","module","month","multiset","national","natural","nchar","nclob","new","no","none","normalize","not","nth_value","ntile","null","nullif","numeric","octet_length","occurrences_regex","of","offset","old","omit","on","one","only","open","or","order","out","outer","over","overlaps","overlay","parameter","partition","pattern","per","percent","percent_rank","percentile_cont","percentile_disc","period","portion","position","position_regex","power","precedes","precision","prepare","primary","procedure","ptf","range","rank","reads","real","recursive","ref","references","referencing","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","release","result","return","returns","revoke","right","rollback","rollup","row","row_number","rows","running","savepoint","scope","scroll","search","second","seek","select","sensitive","session_user","set","show","similar","sin","sinh","skip","smallint","some","specific","specifictype","sql","sqlexception","sqlstate","sqlwarning","sqrt","start","static","stddev_pop","stddev_samp","submultiset","subset","substring","substring_regex","succeeds","sum","symmetric","system","system_time","system_user","table","tablesample","tan","tanh","then","time","timestamp","timezone_hour","timezone_minute","to","trailing","translate","translate_regex","translation","treat","trigger","trim","trim_array","true","truncate","uescape","union","unique","unknown","unnest","update","upper","user","using","value","values","value_of","var_pop","var_samp","varbinary","varchar","varying","versioning","when","whenever","where","width_bucket","window","with","within","without","year","add","asc","collation","desc","final","first","last","view"].filter((e=>!i.includes(e))),l={ +begin:r.concat(/\b/,r.either(...o),/\s*\(/),relevance:0,keywords:{built_in:o}} +;return{name:"SQL",case_insensitive:!0,illegal:/[{}]|<\//,keywords:{ +$pattern:/\b[\w\.]+/,keyword:((e,{exceptions:r,when:t}={})=>{const n=t +;return r=r||[],e.map((e=>e.match(/\|\d+$/)||r.includes(e)?e:n(e)?e+"|0":e)) +})(c,{when:e=>e.length<3}),literal:n,type:a, +built_in:["current_catalog","current_date","current_default_transform_group","current_path","current_role","current_schema","current_transform_group_for_type","current_user","session_user","system_time","system_user","current_time","localtime","current_timestamp","localtimestamp"] +},contains:[{begin:r.either(...s),relevance:0,keywords:{$pattern:/[\w\.]+/, +keyword:c.concat(s),literal:n,type:a}},{className:"type", +begin:r.either("double precision","large object","with timezone","without timezone") +},l,{className:"variable",begin:/@[a-z0-9][a-z0-9_]*/},{className:"string", +variants:[{begin:/'/,end:/'/,contains:[{begin:/''/}]}]},{begin:/"/,end:/"/, +contains:[{begin:/""/}]},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,t,{ +className:"operator",begin:/[-+*/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?/, +relevance:0}]}}})();hljs.registerLanguage("sql",e)})();/*! `swift` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";function e(e){ +return e?"string"==typeof e?e:e.source:null}function n(e){return t("(?=",e,")")} +function t(...n){return n.map((n=>e(n))).join("")}function a(...n){const t=(e=>{ +const n=e[e.length-1] +;return"object"==typeof n&&n.constructor===Object?(e.splice(e.length-1,1),n):{} +})(n);return"("+(t.capture?"":"?:")+n.map((n=>e(n))).join("|")+")"} +const i=e=>t(/\b/,e,/\w$/.test(e)?/\b/:/\B/),s=["Protocol","Type"].map(i),c=["init","self"].map(i),u=["Any","Self"],r=["actor","any","associatedtype","async","await",/as\?/,/as!/,"as","borrowing","break","case","catch","class","consume","consuming","continue","convenience","copy","default","defer","deinit","didSet","distributed","do","dynamic","each","else","enum","extension","fallthrough",/fileprivate\(set\)/,"fileprivate","final","for","func","get","guard","if","import","indirect","infix",/init\?/,/init!/,"inout",/internal\(set\)/,"internal","in","is","isolated","nonisolated","lazy","let","macro","mutating","nonmutating",/open\(set\)/,"open","operator","optional","override","postfix","precedencegroup","prefix",/private\(set\)/,"private","protocol",/public\(set\)/,"public","repeat","required","rethrows","return","set","some","static","struct","subscript","super","switch","throws","throw",/try\?/,/try!/,"try","typealias",/unowned\(safe\)/,/unowned\(unsafe\)/,"unowned","var","weak","where","while","willSet"],o=["false","nil","true"],l=["assignment","associativity","higherThan","left","lowerThan","none","right"],m=["#colorLiteral","#column","#dsohandle","#else","#elseif","#endif","#error","#file","#fileID","#fileLiteral","#filePath","#function","#if","#imageLiteral","#keyPath","#line","#selector","#sourceLocation","#warning"],p=["abs","all","any","assert","assertionFailure","debugPrint","dump","fatalError","getVaList","isKnownUniquelyReferenced","max","min","numericCast","pointwiseMax","pointwiseMin","precondition","preconditionFailure","print","readLine","repeatElement","sequence","stride","swap","swift_unboxFromSwiftValueWithType","transcode","type","unsafeBitCast","unsafeDowncast","withExtendedLifetime","withUnsafeMutablePointer","withUnsafePointer","withVaList","withoutActuallyEscaping","zip"],d=a(/[/=\-+!*%<>&|^~?]/,/[\u00A1-\u00A7]/,/[\u00A9\u00AB]/,/[\u00AC\u00AE]/,/[\u00B0\u00B1]/,/[\u00B6\u00BB\u00BF\u00D7\u00F7]/,/[\u2016-\u2017]/,/[\u2020-\u2027]/,/[\u2030-\u203E]/,/[\u2041-\u2053]/,/[\u2055-\u205E]/,/[\u2190-\u23FF]/,/[\u2500-\u2775]/,/[\u2794-\u2BFF]/,/[\u2E00-\u2E7F]/,/[\u3001-\u3003]/,/[\u3008-\u3020]/,/[\u3030]/),F=a(d,/[\u0300-\u036F]/,/[\u1DC0-\u1DFF]/,/[\u20D0-\u20FF]/,/[\uFE00-\uFE0F]/,/[\uFE20-\uFE2F]/),b=t(d,F,"*"),h=a(/[a-zA-Z_]/,/[\u00A8\u00AA\u00AD\u00AF\u00B2-\u00B5\u00B7-\u00BA]/,/[\u00BC-\u00BE\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF]/,/[\u0100-\u02FF\u0370-\u167F\u1681-\u180D\u180F-\u1DBF]/,/[\u1E00-\u1FFF]/,/[\u200B-\u200D\u202A-\u202E\u203F-\u2040\u2054\u2060-\u206F]/,/[\u2070-\u20CF\u2100-\u218F\u2460-\u24FF\u2776-\u2793]/,/[\u2C00-\u2DFF\u2E80-\u2FFF]/,/[\u3004-\u3007\u3021-\u302F\u3031-\u303F\u3040-\uD7FF]/,/[\uF900-\uFD3D\uFD40-\uFDCF\uFDF0-\uFE1F\uFE30-\uFE44]/,/[\uFE47-\uFEFE\uFF00-\uFFFD]/),f=a(h,/\d/,/[\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/),w=t(h,f,"*"),g=t(/[A-Z]/,f,"*"),y=["attached","autoclosure",t(/convention\(/,a("swift","block","c"),/\)/),"discardableResult","dynamicCallable","dynamicMemberLookup","escaping","freestanding","frozen","GKInspectable","IBAction","IBDesignable","IBInspectable","IBOutlet","IBSegueAction","inlinable","main","nonobjc","NSApplicationMain","NSCopying","NSManaged",t(/objc\(/,w,/\)/),"objc","objcMembers","propertyWrapper","requires_stored_property_inits","resultBuilder","Sendable","testable","UIApplicationMain","unchecked","unknown","usableFromInline","warn_unqualified_access"],E=["iOS","iOSApplicationExtension","macOS","macOSApplicationExtension","macCatalyst","macCatalystApplicationExtension","watchOS","watchOSApplicationExtension","tvOS","tvOSApplicationExtension","swift"] +;return e=>{const d={match:/\s+/,relevance:0},h=e.COMMENT("/\\*","\\*/",{ +contains:["self"]}),A=[e.C_LINE_COMMENT_MODE,h],v={match:[/\./,a(...s,...c)], +className:{2:"keyword"}},C={match:t(/\./,a(...r)),relevance:0 +},N=r.filter((e=>"string"==typeof e)).concat(["_|0"]),k={variants:[{ +className:"keyword", +match:a(...r.filter((e=>"string"!=typeof e)).concat(u).map(i),...c)}]},B={ +$pattern:a(/\b\w+/,/#\w+/),keyword:N.concat(m),literal:o},S=[v,C,k],D=[{ +match:t(/\./,a(...p)),relevance:0},{className:"built_in", +match:t(/\b/,a(...p),/(?=\()/)}],_={match:/->/,relevance:0},M=[_,{ +className:"operator",relevance:0,variants:[{match:b},{match:`\\.(\\.|${F})+`}] +}],x="([0-9]_*)+",$="([0-9a-fA-F]_*)+",L={className:"number",relevance:0, +variants:[{match:`\\b(${x})(\\.(${x}))?([eE][+-]?(${x}))?\\b`},{ +match:`\\b0x(${$})(\\.(${$}))?([pP][+-]?(${x}))?\\b`},{match:/\b0o([0-7]_*)+\b/ +},{match:/\b0b([01]_*)+\b/}]},I=(e="")=>({className:"subst",variants:[{ +match:t(/\\/,e,/[0\\tnr"']/)},{match:t(/\\/,e,/u\{[0-9a-fA-F]{1,8}\}/)}] +}),O=(e="")=>({className:"subst",match:t(/\\/,e,/[\t ]*(?:[\r\n]|\r\n)/) +}),P=(e="")=>({className:"subst",label:"interpol",begin:t(/\\/,e,/\(/),end:/\)/ +}),T=(e="")=>({begin:t(e,/"""/),end:t(/"""/,e),contains:[I(e),O(e),P(e)] +}),K=(e="")=>({begin:t(e,/"/),end:t(/"/,e),contains:[I(e),P(e)]}),j={ +className:"string", +variants:[T(),T("#"),T("##"),T("###"),K(),K("#"),K("##"),K("###")] +},z=[e.BACKSLASH_ESCAPE,{begin:/\[/,end:/\]/,relevance:0, +contains:[e.BACKSLASH_ESCAPE]}],q={begin:/\/[^\s](?=[^/\n]*\/)/,end:/\//, +contains:z},U=e=>{const n=t(e,/\//),a=t(/\//,e);return{begin:n,end:a, +contains:[...z,{scope:"comment",begin:`#(?!.*${a})`,end:/$/}]}},Z={ +scope:"regexp",variants:[U("###"),U("##"),U("#"),q]},V={match:t(/`/,w,/`/) +},W=[V,{className:"variable",match:/\$\d+/},{className:"variable", +match:`\\$${f}+`}],G=[{match:/(@|#(un)?)available/,scope:"keyword",starts:{ +contains:[{begin:/\(/,end:/\)/,keywords:E,contains:[...M,L,j]}]}},{ +scope:"keyword",match:t(/@/,a(...y))},{scope:"meta",match:t(/@/,w)}],H={ +match:n(/\b[A-Z]/),relevance:0,contains:[{className:"type", +match:t(/(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)/,f,"+") +},{className:"type",match:g,relevance:0},{match:/[?!]+/,relevance:0},{ +match:/\.\.\./,relevance:0},{match:t(/\s+&\s+/,n(g)),relevance:0}]},R={ +begin://,keywords:B,contains:[...A,...S,...G,_,H]};H.contains.push(R) +;const X={begin:/\(/,end:/\)/,relevance:0,keywords:B,contains:["self",{ +match:t(w,/\s*:/),keywords:"_|0",relevance:0 +},...A,Z,...S,...D,...M,L,j,...W,...G,H]},J={begin://, +keywords:"repeat each",contains:[...A,H]},Q={begin:/\(/,end:/\)/,keywords:B, +contains:[{begin:a(n(t(w,/\s*:/)),n(t(w,/\s+/,w,/\s*:/))),end:/:/,relevance:0, +contains:[{className:"keyword",match:/\b_\b/},{className:"params",match:w}] +},...A,...S,...M,L,j,...G,H,X],endsParent:!0,illegal:/["']/},Y={ +match:[/(func|macro)/,/\s+/,a(V.match,w,b)],className:{1:"keyword", +3:"title.function"},contains:[J,Q,d],illegal:[/\[/,/%/]},ee={ +match:[/\b(?:subscript|init[?!]?)/,/\s*(?=[<(])/],className:{1:"keyword"}, +contains:[J,Q,d],illegal:/\[|%/},ne={match:[/operator/,/\s+/,b],className:{ +1:"keyword",3:"title"}},te={begin:[/precedencegroup/,/\s+/,g],className:{ +1:"keyword",3:"title"},contains:[H],keywords:[...l,...o],end:/}/} +;for(const e of j.variants){const n=e.contains.find((e=>"interpol"===e.label)) +;n.keywords=B;const t=[...S,...D,...M,L,j,...W];n.contains=[...t,{begin:/\(/, +end:/\)/,contains:["self",...t]}]}return{name:"Swift",keywords:B, +contains:[...A,Y,ee,{beginKeywords:"struct protocol class extension enum actor", +end:"\\{",excludeEnd:!0,keywords:B,contains:[e.inherit(e.TITLE_MODE,{ +className:"title.class",begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/}),...S] +},ne,te,{beginKeywords:"import",end:/$/,contains:[...A],relevance:0 +},Z,...S,...D,...M,L,j,...W,...G,H,X]}}})();hljs.registerLanguage("swift",e) +})();/*! `typescript` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict" +;const e="[A-Za-z$_][0-9A-Za-z$_]*",n=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],a=["true","false","null","undefined","NaN","Infinity"],t=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],s=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],r=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],c=["arguments","this","super","console","window","document","localStorage","sessionStorage","module","global"],i=[].concat(r,t,s) +;function o(o){const l=o.regex,d=e,b={begin:/<[A-Za-z0-9\\._:-]+/, +end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{ +const a=e[0].length+e.index,t=e.input[a] +;if("<"===t||","===t)return void n.ignoreMatch();let s +;">"===t&&(((e,{after:n})=>{const a="",$={ +match:[/const|var|let/,/\s+/,d,/\s*/,/=\s*/,/(async\s*)?/,l.lookahead(B)], +keywords:"async",className:{1:"keyword",3:"title.function"},contains:[R]} +;return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:g,exports:{ +PARAMS_CONTAINS:w,CLASS_REFERENCE:k},illegal:/#(?![$_A-z])/, +contains:[o.SHEBANG({label:"shebang",binary:"node",relevance:5}),{ +label:"use_strict",className:"meta",relevance:10, +begin:/^\s*['"]use (strict|asm)['"]/ +},o.APOS_STRING_MODE,o.QUOTE_STRING_MODE,p,N,f,_,h,{match:/\$\d+/},y,k,{ +className:"attr",begin:d+l.lookahead(":"),relevance:0},$,{ +begin:"("+o.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*", +keywords:"return throw case",relevance:0,contains:[h,o.REGEXP_MODE,{ +className:"function",begin:B,returnBegin:!0,end:"\\s*=>",contains:[{ +className:"params",variants:[{begin:o.UNDERSCORE_IDENT_RE,relevance:0},{ +className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0, +excludeEnd:!0,keywords:g,contains:w}]}]},{begin:/,/,relevance:0},{match:/\s+/, +relevance:0},{variants:[{begin:"<>",end:""},{ +match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:b.begin, +"on:begin":b.isTrulyOpeningTag,end:b.end}],subLanguage:"xml",contains:[{ +begin:b.begin,end:b.end,skip:!0,contains:["self"]}]}]},O,{ +beginKeywords:"while if switch catch for"},{ +begin:"\\b(?!function)"+o.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{", +returnBegin:!0,label:"func.def",contains:[R,o.inherit(o.TITLE_MODE,{begin:d, +className:"title.function"})]},{match:/\.\.\./,relevance:0},T,{match:"\\$"+d, +relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"}, +contains:[R]},C,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/, +className:"variable.constant"},x,M,{match:/\$[(.]/}]}}return t=>{ +const s=o(t),r=e,l=["any","void","number","boolean","string","object","never","symbol","bigint","unknown"],d={ +beginKeywords:"namespace",end:/\{/,excludeEnd:!0, +contains:[s.exports.CLASS_REFERENCE]},b={beginKeywords:"interface",end:/\{/, +excludeEnd:!0,keywords:{keyword:"interface extends",built_in:l}, +contains:[s.exports.CLASS_REFERENCE]},g={$pattern:e, +keyword:n.concat(["type","namespace","interface","public","private","protected","implements","declare","abstract","readonly","enum","override"]), +literal:a,built_in:i.concat(l),"variable.language":c},u={className:"meta", +begin:"@"+r},m=(e,n,a)=>{const t=e.contains.findIndex((e=>e.label===n)) +;if(-1===t)throw Error("can not find mode to replace");e.contains.splice(t,1,a)} +;return Object.assign(s.keywords,g), +s.exports.PARAMS_CONTAINS.push(u),s.contains=s.contains.concat([u,d,b]), +m(s,"shebang",t.SHEBANG()),m(s,"use_strict",{className:"meta",relevance:10, +begin:/^\s*['"]use strict['"]/ +}),s.contains.find((e=>"func.def"===e.label)).relevance=0,Object.assign(s,{ +name:"TypeScript",aliases:["ts","tsx","mts","cts"]}),s}})() +;hljs.registerLanguage("typescript",e)})();/*! `vbnet` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const n=e.regex,t=/\d{1,2}\/\d{1,2}\/\d{4}/,a=/\d{4}-\d{1,2}-\d{1,2}/,i=/(\d|1[012])(:\d+){0,2} *(AM|PM)/,s=/\d{1,2}(:\d{1,2}){1,2}/,r={ +className:"literal",variants:[{begin:n.concat(/# */,n.either(a,t),/ *#/)},{ +begin:n.concat(/# */,s,/ *#/)},{begin:n.concat(/# */,i,/ *#/)},{ +begin:n.concat(/# */,n.either(a,t),/ +/,n.either(i,s),/ *#/)}] +},l=e.COMMENT(/'''/,/$/,{contains:[{className:"doctag",begin:/<\/?/,end:/>/}] +}),o=e.COMMENT(null,/$/,{variants:[{begin:/'/},{begin:/([\t ]|^)REM(?=\s)/}]}) +;return{name:"Visual Basic .NET",aliases:["vb"],case_insensitive:!0, +classNameAliases:{label:"symbol"},keywords:{ +keyword:"addhandler alias aggregate ansi as async assembly auto binary by byref byval call case catch class compare const continue custom declare default delegate dim distinct do each equals else elseif end enum erase error event exit explicit finally for friend from function get global goto group handles if implements imports in inherits interface into iterator join key let lib loop me mid module mustinherit mustoverride mybase myclass namespace narrowing new next notinheritable notoverridable of off on operator option optional order overloads overridable overrides paramarray partial preserve private property protected public raiseevent readonly redim removehandler resume return select set shadows shared skip static step stop structure strict sub synclock take text then throw to try unicode until using when where while widening with withevents writeonly yield", +built_in:"addressof and andalso await directcast gettype getxmlnamespace is isfalse isnot istrue like mod nameof new not or orelse trycast typeof xor cbool cbyte cchar cdate cdbl cdec cint clng cobj csbyte cshort csng cstr cuint culng cushort", +type:"boolean byte char date decimal double integer long object sbyte short single string uinteger ulong ushort", +literal:"true false nothing"}, +illegal:"//|\\{|\\}|endif|gosub|variant|wend|^\\$ ",contains:[{ +className:"string",begin:/"(""|[^/n])"C\b/},{className:"string",begin:/"/, +end:/"/,illegal:/\n/,contains:[{begin:/""/}]},r,{className:"number",relevance:0, +variants:[{begin:/\b\d[\d_]*((\.[\d_]+(E[+-]?[\d_]+)?)|(E[+-]?[\d_]+))[RFD@!#]?/ +},{begin:/\b\d[\d_]*((U?[SIL])|[%&])?/},{begin:/&H[\dA-F_]+((U?[SIL])|[%&])?/},{ +begin:/&O[0-7_]+((U?[SIL])|[%&])?/},{begin:/&B[01_]+((U?[SIL])|[%&])?/}]},{ +className:"label",begin:/^\w+:/},l,o,{className:"meta", +begin:/[\t ]*#(const|disable|else|elseif|enable|end|externalsource|if|region)\b/, +end:/$/,keywords:{ +keyword:"const disable else elseif enable end externalsource if region then"}, +contains:[o]}]}}})();hljs.registerLanguage("vbnet",e)})();/*! `wasm` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";return e=>{e.regex;const a=e.COMMENT(/\(;/,/;\)/) +;return a.contains.push("self"),{name:"WebAssembly",keywords:{$pattern:/[\w.]+/, +keyword:["anyfunc","block","br","br_if","br_table","call","call_indirect","data","drop","elem","else","end","export","func","global.get","global.set","local.get","local.set","local.tee","get_global","get_local","global","if","import","local","loop","memory","memory.grow","memory.size","module","mut","nop","offset","param","result","return","select","set_global","set_local","start","table","tee_local","then","type","unreachable"] +},contains:[e.COMMENT(/;;/,/$/),a,{match:[/(?:offset|align)/,/\s*/,/=/], +className:{1:"keyword",3:"operator"}},{className:"variable",begin:/\$[\w_]+/},{ +match:/(\((?!;)|\))+/,className:"punctuation",relevance:0},{ +begin:[/(?:func|call|call_indirect)/,/\s+/,/\$[^\s)]+/],className:{1:"keyword", +3:"title.function"}},e.QUOTE_STRING_MODE,{match:/(i32|i64|f32|f64)(?!\.)/, +className:"type"},{className:"keyword", +match:/\b(f32|f64|i32|i64)(?:\.(?:abs|add|and|ceil|clz|const|convert_[su]\/i(?:32|64)|copysign|ctz|demote\/f64|div(?:_[su])?|eqz?|extend_[su]\/i32|floor|ge(?:_[su])?|gt(?:_[su])?|le(?:_[su])?|load(?:(?:8|16|32)_[su])?|lt(?:_[su])?|max|min|mul|nearest|neg?|or|popcnt|promote\/f32|reinterpret\/[fi](?:32|64)|rem_[su]|rot[lr]|shl|shr_[su]|store(?:8|16|32)?|sqrt|sub|trunc(?:_[su]\/f(?:32|64))?|wrap\/i64|xor))\b/ +},{className:"number",relevance:0, +match:/[+-]?\b(?:\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?|0x[\da-fA-F](?:_?[\da-fA-F])*(?:\.[\da-fA-F](?:_?[\da-fA-D])*)?(?:[pP][+-]?\d(?:_?\d)*)?)\b|\binf\b|\bnan(?::0x[\da-fA-F](?:_?[\da-fA-D])*)?\b/ +}]}}})();hljs.registerLanguage("wasm",e)})();/*! `xml` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const a=e.regex,n=a.concat(/[\p{L}_]/u,a.optional(/[\p{L}0-9_.-]*:/u),/[\p{L}0-9_.-]*/u),s={ +className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},t={begin:/\s/, +contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}] +},i=e.inherit(t,{begin:/\(/,end:/\)/}),c=e.inherit(e.APOS_STRING_MODE,{ +className:"string"}),l=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),r={ +endsWithParent:!0,illegal:/`]+/}]}]}]};return{ +name:"HTML, XML", +aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"], +case_insensitive:!0,unicodeRegex:!0,contains:[{className:"meta",begin://,relevance:10,contains:[t,l,c,i,{begin:/\[/,end:/\]/,contains:[{ +className:"meta",begin://,contains:[t,i,l,c]}]}] +},e.COMMENT(//,{relevance:10}),{begin://, +relevance:10},s,{className:"meta",end:/\?>/,variants:[{begin:/<\?xml/, +relevance:10,contains:[l]},{begin:/<\?[a-z][a-z0-9]+/}]},{className:"tag", +begin:/)/,end:/>/,keywords:{name:"style"},contains:[r],starts:{ +end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag", +begin:/)/,end:/>/,keywords:{name:"script"},contains:[r],starts:{ +end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{ +className:"tag",begin:/<>|<\/>/},{className:"tag", +begin:a.concat(//,/>/,/\s/)))), +end:/\/?>/,contains:[{className:"name",begin:n,relevance:0,starts:r}]},{ +className:"tag",begin:a.concat(/<\//,a.lookahead(a.concat(n,/>/))),contains:[{ +className:"name",begin:n,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]}} +})();hljs.registerLanguage("xml",e)})();/*! `yaml` grammar compiled for Highlight.js 11.9.0 */ +(()=>{var e=(()=>{"use strict";return e=>{ +const n="true false yes no null",a="[\\w#;/?:@&=+$,.~*'()[\\]]+",s={ +className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/ +},{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable", +variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},i=e.inherit(s,{ +variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),l={ +end:",",endsWithParent:!0,excludeEnd:!0,keywords:n,relevance:0},t={begin:/\{/, +end:/\}/,contains:[l],illegal:"\\n",relevance:0},g={begin:"\\[",end:"\\]", +contains:[l],illegal:"\\n",relevance:0},b=[{className:"attr",variants:[{ +begin:/\w[\w :()\./-]*:(?=[ \t]|$)/},{begin:/"\w[\w :()\./-]*":(?=[ \t]|$)/},{ +begin:/'\w[\w :()\./-]*':(?=[ \t]|$)/}]},{className:"meta",begin:"^---\\s*$", +relevance:10},{className:"string", +begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{ +begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0, +relevance:0},{className:"type",begin:"!\\w+!"+a},{className:"type", +begin:"!<"+a+">"},{className:"type",begin:"!"+a},{className:"type",begin:"!!"+a +},{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta", +begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)", +relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:n,keywords:{literal:n}},{ +className:"number", +begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b" +},{className:"number",begin:e.C_NUMBER_RE+"\\b",relevance:0},t,g,s],r=[...b] +;return r.pop(),r.push(i),l.contains=r,{name:"YAML",case_insensitive:!0, +aliases:["yml"],contains:b}}})();hljs.registerLanguage("yaml",e)})(); \ No newline at end of file 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 5502735e..0fd65f19 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 @@ -68,11 +68,11 @@ -fx-background-color: -color-neutral-muted; } -.store-entry-comp .button-bar, .store-entry-comp .dropdown-comp { +.store-entry-comp .button-bar, .store-entry-comp .dropdown-comp, .store-entry-comp .toggle-switch-comp { -fx-opacity: 0.65; } -.store-entry-comp:hover .button-bar, .store-entry-comp:hover .dropdown-comp { +.store-entry-comp:hover .button-bar, .store-entry-comp:hover .dropdown-comp, .store-entry-comp:hover .toggle-switch-comp { -fx-opacity: 1.0; } 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 16993e30..2d896847 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 @@ -5,6 +5,14 @@ } */ +.toggle-switch:has-graphic .label { + -fx-font-size: 1.7em; +} + +.toggle-switch:has-graphic { + -fx-font-size: 0.8em; +} + .store-layout .split-pane-divider { -fx-background-color: transparent; } diff --git a/app/src/main/resources/io/xpipe/app/resources/theme/custom.css b/app/src/main/resources/io/xpipe/app/resources/theme/custom.css index 9c93a834..6fb386ee 100644 --- a/app/src/main/resources/io/xpipe/app/resources/theme/custom.css +++ b/app/src/main/resources/io/xpipe/app/resources/theme/custom.css @@ -59,7 +59,7 @@ -color-bg-overlay: rgb(28, 28, 30); -color-bg-subtle: rgb(44, 44, 46); -color-bg-inset: #0b0b0c; - -color-border-default: rgb(255, 255, 255); + -color-border-default: #30363d; -color-border-muted: rgb(58, 58, 60); -color-border-subtle: rgb(49, 49, 52); -color-shadow-default: rgb(0, 0, 0); diff --git a/app/src/main/resources/io/xpipe/app/resources/theme/mocha.css b/app/src/main/resources/io/xpipe/app/resources/theme/mocha.css new file mode 100644 index 00000000..fece551f --- /dev/null +++ b/app/src/main/resources/io/xpipe/app/resources/theme/mocha.css @@ -0,0 +1,115 @@ +.root { + -color-dark: rgb(255, 255, 255); + -color-light: rgb(0, 0, 0); + -color-base-0: #f2f2f7; + -color-base-1: #e5e5ea; + -color-base-2: #d1d1d6; + -color-base-3: #aeaeb2; + -color-base-4: rgb(142, 142, 147); + -color-base-5: rgb(99, 99, 102); + -color-base-6: rgb(72, 72, 74); + -color-base-7: rgb(58, 58, 60); + -color-base-8: rgb(44, 44, 46); + -color-base-9: rgb(28, 28, 30); + -color-accent-0: #c2e0ff; + -color-accent-1: #9dceff; + -color-accent-2: #78bbff; + -color-accent-3: #54a9ff; + -color-accent-4: #2f96ff; + -color-accent-5: rgb(10, 132, 255); + -color-accent-6: #0970d9; + -color-accent-7: #075cb3; + -color-accent-8: #06498c; + -color-accent-9: #043566; + -color-success-0: #ccf5d2; + -color-success-1: #adefb7; + -color-success-2: #8ee99c; + -color-success-3: #70e381; + -color-success-4: #51dd66; + -color-success-5: rgb(50, 215, 75); + -color-success-6: #2bb740; + -color-success-7: #239735; + -color-success-8: #1c7629; + -color-success-9: #14561e; + -color-warning-0: #ffe7c2; + -color-warning-1: #ffd99d; + -color-warning-2: #ffca78; + -color-warning-3: #ffbc54; + -color-warning-4: #ffad2f; + -color-warning-5: rgb(255, 159, 10); + -color-warning-6: #d98709; + -color-warning-7: #b36f07; + -color-warning-8: #8c5706; + -color-warning-9: #664004; + -color-danger-0: #ffd1ce; + -color-danger-1: #ffb5b0; + -color-danger-2: #ff9993; + -color-danger-3: #ff7d75; + -color-danger-4: #ff6158; + -color-danger-5: rgb(255, 69, 58); + -color-danger-6: #d93b31; + -color-danger-7: #b33029; + -color-danger-8: #8c2620; + -color-danger-9: #661c17; + -color-fg-default: rgb(205, 214, 244); + -color-fg-muted: rgb(186, 194, 222); + -color-fg-subtle: rgb(166, 173, 200); + -color-fg-emphasis: rgb(180, 190, 254); + -color-bg-default: rgb(30, 30, 46); + -color-bg-overlay: rgb(24, 24, 37); + -color-bg-subtle: rgb(49, 50, 68); + -color-bg-inset: rgb(17, 17, 27); + -color-border-default: #30363d; + -color-border-muted: rgb(58, 58, 60); + -color-border-subtle: rgb(49, 49, 52); + -color-shadow-default: rgb(0, 0, 0); + -color-neutral-emphasis-plus: rgb(142, 142, 147); + -color-neutral-emphasis: rgb(142, 142, 147); + -color-neutral-muted: rgba(99, 99, 102, 0.4); + -color-neutral-subtle: rgba(99, 99, 102, 0.1); + -color-accent-fg: #2f96ff; + -color-accent-emphasis: rgb(10, 132, 255); + -color-accent-muted: rgba(10, 132, 255, 0.4); + -color-accent-subtle: rgba(10, 132, 255, 0.15); + -color-warning-fg: rgb(255, 159, 10); + -color-warning-emphasis: #d98709; + -color-warning-muted: rgba(255, 159, 10, 0.4); + -color-warning-subtle: rgba(255, 159, 10, 0.15); + -color-success-fg: rgb(50, 215, 75); + -color-success-emphasis: #2bb740; + -color-success-muted: rgba(50, 215, 75, 0.4); + -color-success-subtle: rgba(50, 215, 75, 0.15); + -color-danger-fg: rgb(255, 69, 58); + -color-danger-emphasis: rgb(255, 69, 58); + -color-danger-muted: rgba(255, 69, 58, 0.4); + -color-danger-subtle: rgba(255, 69, 58, 0.15); + -color-chart-1: #f3622d; + -color-chart-2: #fba71b; + -color-chart-3: #57b757; + -color-chart-4: #41a9c9; + -color-chart-5: #4258c9; + -color-chart-6: #9a42c8; + -color-chart-7: #c84164; + -color-chart-8: #888888; + -color-chart-1-alpha70: rgba(243, 98, 45, 0.7); + -color-chart-2-alpha70: rgba(251, 167, 27, 0.7); + -color-chart-3-alpha70: rgba(87, 183, 87, 0.7); + -color-chart-4-alpha70: rgba(65, 169, 201, 0.7); + -color-chart-5-alpha70: rgba(66, 88, 201, 0.7); + -color-chart-6-alpha70: rgba(154, 66, 200, 0.7); + -color-chart-7-alpha70: rgba(200, 65, 100, 0.7); + -color-chart-8-alpha70: rgba(136, 136, 136, 0.7); + -color-chart-1-alpha20: rgba(243, 98, 45, 0.2); + -color-chart-2-alpha20: rgba(251, 167, 27, 0.2); + -color-chart-3-alpha20: rgba(87, 183, 87, 0.2); + -color-chart-4-alpha20: rgba(65, 169, 201, 0.2); + -color-chart-5-alpha20: rgba(66, 88, 201, 0.2); + -color-chart-6-alpha20: rgba(154, 66, 200, 0.2); + -color-chart-7-alpha20: rgba(200, 65, 100, 0.2); + -color-chart-8-alpha20: rgba(136, 136, 136, 0.2); + -fx-background-color: -color-bg-default; + -fx-font-size: 14px; + -fx-background-radius: inherit; + -fx-background-insets: inherit; + -fx-padding: inherit; +} \ No newline at end of file diff --git a/beacon/README.md b/beacon/README.md index 54ed3fc1..5563a5fb 100644 --- a/beacon/README.md +++ b/beacon/README.md @@ -4,36 +4,27 @@ ## XPipe Beacon The XPipe beacon component is responsible for handling all communications between the XPipe daemon -and the various programming language APIs and the CLI. It provides an API that supports all kinds +and the APIs and the CLI. It provides an API that supports all kinds of different operations. ### Inner Workings -- The underlying inter-process communication is realized through - TCP sockets on port `21721` on Windows and `21723` on Linux/macOS. +- The underlying communication is realized through an HTTP server on port `21721` - The data structures and exchange protocols are specified in the - [io.xpipe.beacon.exchange package](src/main/java/io/xpipe/beacon/exchange). + [io.xpipe.beacon.api package](src/main/java/io/xpipe/beacon/api). - Every exchange is initiated from the outside by sending a request message to the XPipe daemon. The daemon then always sends a response message. -- The header information of a message is formatted in the json format. +- The body of a message is formatted in the json format. As a result, all data structures exchanged must be serializable/deserializable with jackson. -- Both the requests and responses can optionally include content in a body. - A body is initiated with two new lines (`\n`). - -- The body is split into segments of max length `65536`. - Each segment is preceded by four bytes that specify the length of the next segment. - In case the next segment has a length of less than `65536` bytes, we know that the end of the body has been reached. - This way the socket communication can handle payloads of unknown length. - ## Configuration #### Custom port -The default port can be changed by passing the property `io.xpipe.beacon.port=` to both the daemon and APIs. +The default port can be changed by passing the property `io.xpipe.beacon.port=` to the daemon or changing it in the settings menu. Note that if both sides do not have the same port setting, they won't be able to reach each other. #### Custom launch command diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconAuthMethod.java b/beacon/src/main/java/io/xpipe/beacon/BeaconAuthMethod.java new file mode 100644 index 00000000..9a458bd7 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconAuthMethod.java @@ -0,0 +1,34 @@ +package io.xpipe.beacon; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + property = "type") +public interface BeaconAuthMethod { + + @JsonTypeName("Local") + @Value + @Builder + @Jacksonized + public static class Local implements BeaconAuthMethod { + + @NonNull + String authFileContent; + } + + @JsonTypeName("ApiKey") + @Value + @Builder + @Jacksonized + public static class ApiKey implements BeaconAuthMethod { + + @NonNull + String key; + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java index 8ccf6b9e..80559fc1 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java @@ -1,314 +1,129 @@ package io.xpipe.beacon; -import io.xpipe.beacon.exchange.MessageExchanges; -import io.xpipe.beacon.exchange.data.ClientErrorMessage; -import io.xpipe.beacon.exchange.data.ServerErrorMessage; -import io.xpipe.core.util.Deobfuscator; -import io.xpipe.core.util.JacksonMapper; - -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; -import lombok.Builder; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; +import io.xpipe.beacon.api.HandshakeExchange; +import io.xpipe.core.util.JacksonMapper; +import io.xpipe.core.util.XPipeInstallation; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.StringWriter; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; import java.util.Optional; -import static io.xpipe.beacon.BeaconConfig.BODY_SEPARATOR; +public class BeaconClient { -public class BeaconClient implements AutoCloseable { + private final int port; + private String token; - @Getter - private final AutoCloseable base; + public BeaconClient(int port) {this.port = port;} - private final InputStream in; - private final OutputStream out; - - private BeaconClient(AutoCloseable base, InputStream in, OutputStream out) { - this.base = base; - this.in = in; - this.out = out; - } - - public static BeaconClient establishConnection(ClientInformation information) throws Exception { - var socket = new Socket(); - socket.connect(new InetSocketAddress(InetAddress.getLoopbackAddress(), BeaconConfig.getUsedPort()), 5000); - socket.setSoTimeout(5000); - var client = new BeaconClient(socket, socket.getInputStream(), socket.getOutputStream()); - client.sendObject(JacksonMapper.getDefault().valueToTree(information)); - var res = client.receiveObject(); - if (!res.isTextual() || !"ACK".equals(res.asText())) { - throw new BeaconException("Daemon responded with invalid acknowledgement"); - } - socket.setSoTimeout(0); + public static BeaconClient establishConnection(int port, BeaconClientInformation information) throws Exception { + var client = new BeaconClient(port); + var auth = Files.readString(XPipeInstallation.getLocalBeaconAuthFile()); + HandshakeExchange.Response response = client.performRequest(HandshakeExchange.Request.builder() + .client(information) + .auth(BeaconAuthMethod.Local.builder().authFileContent(auth).build()).build()); + client.token = response.getSessionToken(); return client; } - public static Optional tryEstablishConnection(ClientInformation information) { + public static Optional tryEstablishConnection(int port, BeaconClientInformation information) { try { - return Optional.of(establishConnection(information)); + return Optional.of(establishConnection(port, information)); } catch (Exception ex) { return Optional.empty(); } } - public void close() throws ConnectorException { - try { - base.close(); - } catch (Exception ex) { - throw new ConnectorException("Couldn't close client", ex); - } - } - public InputStream receiveBody() throws ConnectorException { - try { - var sep = in.readNBytes(BODY_SEPARATOR.length); - if (sep.length != 0 && !Arrays.equals(BODY_SEPARATOR, sep)) { - throw new ConnectorException("Invalid body separator"); - } - return BeaconFormat.readBlocks(in); - } catch (IOException ex) { - throw new ConnectorException(ex); - } - } - - public OutputStream sendBody() throws ConnectorException { - try { - out.write(BODY_SEPARATOR); - return BeaconFormat.writeBlocks(out); - } catch (IOException ex) { - throw new ConnectorException(ex); - } - } - - public void sendRequest(T req) throws ClientException, ConnectorException { - ObjectNode json = JacksonMapper.getDefault().valueToTree(req); - var prov = MessageExchanges.byRequest(req); - if (prov.isEmpty()) { - throw new ClientException("Unknown request class " + req.getClass()); - } - - json.set("messageType", new TextNode(prov.get().getId())); - json.set("messagePhase", new TextNode("request")); - // json.set("id", new TextNode(UUID.randomUUID().toString())); - var msg = JsonNodeFactory.instance.objectNode(); - msg.set("xPipeMessage", json); - - if (BeaconConfig.printMessages()) { - System.out.println( - "Sending request to server of type " + req.getClass().getName()); - } - - sendObject(msg); - } - - public void sendEOF() throws ConnectorException { - try (OutputStream ignored = BeaconFormat.writeBlocks(out)) { - } catch (IOException ex) { - throw new ConnectorException("Couldn't write to socket", ex); - } - } - - public void sendObject(JsonNode node) throws ConnectorException { - var writer = new StringWriter(); - var mapper = JacksonMapper.getDefault(); - try (JsonGenerator g = mapper.createGenerator(writer).setPrettyPrinter(new DefaultPrettyPrinter())) { - g.writeTree(node); - } catch (IOException ex) { - throw new ConnectorException("Couldn't serialize request", ex); - } - - var content = writer.toString(); + @SuppressWarnings("unchecked") + public RES performRequest(BeaconInterface prov, String rawNode) throws + BeaconConnectorException, BeaconClientException, BeaconServerException { + var content = rawNode; if (BeaconConfig.printMessages()) { System.out.println("Sending raw request:"); System.out.println(content); } - try (OutputStream blockOut = BeaconFormat.writeBlocks(out)) { - blockOut.write(content.getBytes(StandardCharsets.UTF_8)); - } catch (IOException ex) { - throw new ConnectorException("Couldn't write to socket", ex); - } - } - - public T receiveResponse() throws ConnectorException, ClientException, ServerException { - return parseResponse(receiveObject()); - } - - private JsonNode receiveObject() throws ConnectorException, ClientException, ServerException { - JsonNode node; - try (InputStream blockIn = BeaconFormat.readBlocks(in)) { - node = JacksonMapper.getDefault().readTree(blockIn); - } catch (IOException ex) { - throw new ConnectorException("Couldn't read from socket", ex); + var client = HttpClient.newHttpClient(); + HttpResponse response; + try { + var uri = URI.create("http://localhost:" + port + prov.getPath()); + var builder = HttpRequest.newBuilder(); + if (token != null) { + builder.header("Authorization", "Bearer " + token); + } + var httpRequest = builder + .uri(uri).POST(HttpRequest.BodyPublishers.ofString(content)).build(); + response = client.send(httpRequest, HttpResponse.BodyHandlers.ofString()); + } catch (Exception ex) { + throw new BeaconConnectorException("Couldn't send request", ex); } if (BeaconConfig.printMessages()) { - System.out.println("Received response:"); - System.out.println(node.toPrettyString()); + System.out.println("Received raw response:"); + System.out.println(response.body()); } - if (node.isMissingNode()) { - throw new ConnectorException("Received unexpected EOF"); - } - - var se = parseServerError(node); + var se = parseServerError(response); if (se.isPresent()) { se.get().throwError(); } - var ce = parseClientError(node); + var ce = parseClientError(response); if (ce.isPresent()) { throw ce.get().throwException(); } - return node; - } - - private Optional parseClientError(JsonNode node) throws ConnectorException { - ObjectNode content = (ObjectNode) node.get("xPipeClientError"); - if (content == null) { - return Optional.empty(); - } - try { - var message = JacksonMapper.getDefault().treeToValue(content, ClientErrorMessage.class); - return Optional.of(message); + var reader = JacksonMapper.getDefault().readerFor(prov.getResponseClass()); + var v = (RES) reader.readValue(response.body()); + return v; } catch (IOException ex) { - throw new ConnectorException("Couldn't parse client error message", ex); + throw new BeaconConnectorException("Couldn't parse response", ex); } } - private Optional parseServerError(JsonNode node) throws ConnectorException { - ObjectNode content = (ObjectNode) node.get("xPipeServerError"); - if (content == null) { - return Optional.empty(); - } - - try { - var message = JacksonMapper.getDefault().treeToValue(content, ServerErrorMessage.class); - Deobfuscator.deobfuscate(message.getError()); - return Optional.of(message); - } catch (IOException ex) { - throw new ConnectorException("Couldn't parse server error message", ex); - } - } - - private T parseResponse(JsonNode header) throws ConnectorException { - ObjectNode content = (ObjectNode) header.required("xPipeMessage"); - - var type = content.required("messageType").textValue(); - var phase = content.required("messagePhase").textValue(); - // var requestId = UUID.fromString(content.required("id").textValue()); - if (!phase.equals("response")) { - throw new IllegalArgumentException(); - } - content.remove("messageType"); - content.remove("messagePhase"); - // content.remove("id"); - - var prov = MessageExchanges.byId(type); + public RES performRequest(REQ req) throws BeaconConnectorException, BeaconClientException, BeaconServerException { + ObjectNode node = JacksonMapper.getDefault().valueToTree(req); + var prov = BeaconInterface.byRequest(req); if (prov.isEmpty()) { - throw new IllegalArgumentException("Unknown response id " + type); + throw new IllegalArgumentException("Unknown request class " + req.getClass()); + } + if (BeaconConfig.printMessages()) { + System.out.println("Sending request to server of type " + req.getClass().getName()); + } + + return performRequest(prov.get(), node.toPrettyString()); + } + + private Optional parseClientError(HttpResponse response) throws BeaconConnectorException { + if (response.statusCode() < 400 || response.statusCode() > 499) { + return Optional.empty(); } try { - var reader = JacksonMapper.getDefault().readerFor(prov.get().getResponseClass()); - return reader.readValue(content); + var v = JacksonMapper.getDefault().readValue(response.body(), BeaconClientErrorResponse.class); + return Optional.of(v); } catch (IOException ex) { - throw new ConnectorException("Couldn't parse response", ex); + throw new BeaconConnectorException("Couldn't parse client error message", ex); } } - public InputStream getRawInputStream() { - return in; - } - - public OutputStream getRawOutputStream() { - return out; - } - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") - public abstract static class ClientInformation { - - public final CliClientInformation cli() { - return (CliClientInformation) this; + private Optional parseServerError(HttpResponse response) throws BeaconConnectorException { + if (response.statusCode() < 500 || response.statusCode() > 599) { + return Optional.empty(); } - public abstract String toDisplayString(); - } - - @JsonTypeName("cli") - @Value - @Builder - @Jacksonized - @EqualsAndHashCode(callSuper = false) - public static class CliClientInformation extends ClientInformation { - - @Override - public String toDisplayString() { - return "XPipe CLI"; + try { + var v = JacksonMapper.getDefault().readValue(response.body(), BeaconServerErrorResponse.class); + return Optional.of(v); + } catch (IOException ex) { + throw new BeaconConnectorException("Couldn't parse client error message", ex); } } - @JsonTypeName("daemon") - @Value - @Builder - @Jacksonized - @EqualsAndHashCode(callSuper = false) - public static class DaemonInformation extends ClientInformation { - - @Override - public String toDisplayString() { - return "Daemon"; - } - } - - @JsonTypeName("gateway") - @Value - @Builder - @Jacksonized - @EqualsAndHashCode(callSuper = false) - public static class GatewayClientInformation extends ClientInformation { - - String version; - - @Override - public String toDisplayString() { - return "XPipe Gateway " + version; - } - } - - @JsonTypeName("api") - @Value - @Builder - @Jacksonized - @EqualsAndHashCode(callSuper = false) - public static class ApiClientInformation extends ClientInformation { - - String version; - String language; - - @Override - public String toDisplayString() { - return String.format("XPipe %s API v%s", language, version); - } - } } diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/data/ClientErrorMessage.java b/beacon/src/main/java/io/xpipe/beacon/BeaconClientErrorResponse.java similarity index 53% rename from beacon/src/main/java/io/xpipe/beacon/exchange/data/ClientErrorMessage.java rename to beacon/src/main/java/io/xpipe/beacon/BeaconClientErrorResponse.java index 09fd318e..bd2c196a 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/data/ClientErrorMessage.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconClientErrorResponse.java @@ -1,6 +1,4 @@ -package io.xpipe.beacon.exchange.data; - -import io.xpipe.beacon.ClientException; +package io.xpipe.beacon; import lombok.AllArgsConstructor; import lombok.Builder; @@ -12,11 +10,11 @@ import lombok.extern.jackson.Jacksonized; @Builder @Jacksonized @AllArgsConstructor -public class ClientErrorMessage { +public class BeaconClientErrorResponse { String message; - public ClientException throwException() { - return new ClientException(message); + public BeaconClientException throwException() { + return new BeaconClientException(message); } } diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconClientException.java b/beacon/src/main/java/io/xpipe/beacon/BeaconClientException.java new file mode 100644 index 00000000..a561c777 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconClientException.java @@ -0,0 +1,23 @@ +package io.xpipe.beacon; + +/** + * Indicates that a client request was invalid. + */ +public class BeaconClientException extends Exception { + + public BeaconClientException(String message) { + super(message); + } + + public BeaconClientException(String message, Throwable cause) { + super(message, cause); + } + + public BeaconClientException(Throwable cause) { + super(cause); + } + + public BeaconClientException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconClientInformation.java b/beacon/src/main/java/io/xpipe/beacon/BeaconClientInformation.java new file mode 100644 index 00000000..a0841e35 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconClientInformation.java @@ -0,0 +1,59 @@ +package io.xpipe.beacon; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + property = "type") +public abstract class BeaconClientInformation { + + public abstract String toDisplayString(); + + @JsonTypeName("Cli") + @Value + @Builder + @Jacksonized + @EqualsAndHashCode(callSuper = false) + public static class Cli extends BeaconClientInformation { + + @Override + public String toDisplayString() { + return "XPipe CLI"; + } + } + + @JsonTypeName("Daemon") + @Value + @Builder + @Jacksonized + @EqualsAndHashCode(callSuper = false) + public static class Daemon extends BeaconClientInformation { + + @Override + public String toDisplayString() { + return "Daemon"; + } + } + + @JsonTypeName("Api") + @Value + @Builder + @Jacksonized + @EqualsAndHashCode(callSuper = false) + public static class Api extends BeaconClientInformation { + + @NonNull + String name; + + @Override + public String toDisplayString() { + return name; + } + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconConfig.java b/beacon/src/main/java/io/xpipe/beacon/BeaconConfig.java index 4e21f3d5..c62af056 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconConfig.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconConfig.java @@ -1,15 +1,11 @@ package io.xpipe.beacon; import io.xpipe.core.util.XPipeInstallation; - import lombok.experimental.UtilityClass; -import java.nio.charset.StandardCharsets; - @UtilityClass public class BeaconConfig { - public static final byte[] BODY_SEPARATOR = "\n\n".getBytes(StandardCharsets.UTF_8); public static final String BEACON_PORT_PROP = "io.xpipe.beacon.port"; public static final String DAEMON_ARGUMENTS_PROP = "io.xpipe.beacon.daemonArgs"; private static final String PRINT_MESSAGES_PROPERTY = "io.xpipe.beacon.printMessages"; @@ -17,14 +13,6 @@ public class BeaconConfig { private static final String ATTACH_DEBUGGER_PROP = "io.xpipe.beacon.attachDebuggerToDaemon"; private static final String EXEC_DEBUG_PROP = "io.xpipe.beacon.printDaemonOutput"; private static final String EXEC_PROCESS_PROP = "io.xpipe.beacon.customDaemonCommand"; - private static final String LOCAL_PROXY_PROP = "io.xpipe.beacon.localProxy"; - - public static boolean localProxy() { - if (System.getProperty(LOCAL_PROXY_PROP) != null) { - return Boolean.parseBoolean(System.getProperty(LOCAL_PROXY_PROP)); - } - return false; - } public static boolean printMessages() { if (System.getProperty(PRINT_MESSAGES_PROPERTY) != null) { diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconConnection.java b/beacon/src/main/java/io/xpipe/beacon/BeaconConnection.java deleted file mode 100644 index 509e6d71..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconConnection.java +++ /dev/null @@ -1,189 +0,0 @@ -package io.xpipe.beacon; - -import io.xpipe.core.util.FailableBiConsumer; -import io.xpipe.core.util.FailableConsumer; - -import lombok.Getter; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -public abstract class BeaconConnection implements AutoCloseable { - - @Getter - protected BeaconClient beaconClient; - - private InputStream bodyInput; - private OutputStream bodyOutput; - - protected abstract void constructSocket(); - - @Override - public void close() { - try { - if (beaconClient != null) { - beaconClient.close(); - } - beaconClient = null; - } catch (Exception e) { - beaconClient = null; - throw new BeaconException("Could not close beacon connection", e); - } - } - - public void withOutputStream(FailableConsumer ex) { - try { - ex.accept(getOutputStream()); - } catch (IOException e) { - throw new BeaconException("Could not write to beacon output stream", e); - } - } - - public void withInputStream(FailableConsumer ex) { - try { - ex.accept(getInputStream()); - } catch (IOException e) { - throw new BeaconException("Could not read from beacon output stream", e); - } - } - - public void checkClosed() { - if (beaconClient == null) { - throw new BeaconException("Socket is closed"); - } - } - - public OutputStream getOutputStream() { - checkClosed(); - - if (bodyOutput == null) { - throw new IllegalStateException("Body output has not started yet"); - } - - return bodyOutput; - } - - public InputStream getInputStream() { - checkClosed(); - - if (bodyInput == null) { - throw new IllegalStateException("Body input has not started yet"); - } - - return bodyInput; - } - - public void performInputExchange( - REQ req, FailableBiConsumer responseConsumer) { - checkClosed(); - - performInputOutputExchange(req, null, responseConsumer); - } - - public void performInputOutputExchange( - REQ req, - FailableConsumer reqWriter, - FailableBiConsumer responseConsumer) { - checkClosed(); - - try { - beaconClient.sendRequest(req); - if (reqWriter != null) { - try (var out = sendBody()) { - reqWriter.accept(out); - } - } - RES res = beaconClient.receiveResponse(); - try (var in = receiveBody()) { - responseConsumer.accept(res, in); - } - } catch (Exception e) { - throw unwrapException(e); - } - } - - public void sendRequest(REQ req) { - checkClosed(); - - try { - beaconClient.sendRequest(req); - } catch (Exception e) { - throw unwrapException(e); - } - } - - public RES receiveResponse() { - checkClosed(); - - try { - return beaconClient.receiveResponse(); - } catch (Exception e) { - throw unwrapException(e); - } - } - - public OutputStream sendBody() { - checkClosed(); - - try { - bodyOutput = beaconClient.sendBody(); - return bodyOutput; - } catch (Exception e) { - throw unwrapException(e); - } - } - - public InputStream receiveBody() { - checkClosed(); - - try { - bodyInput = beaconClient.receiveBody(); - return bodyInput; - } catch (Exception e) { - throw unwrapException(e); - } - } - - public RES performOutputExchange( - REQ req, FailableConsumer reqWriter) { - checkClosed(); - - try { - beaconClient.sendRequest(req); - try (var out = sendBody()) { - reqWriter.accept(out); - } - return beaconClient.receiveResponse(); - } catch (Exception e) { - throw unwrapException(e); - } - } - - public RES performSimpleExchange(REQ req) { - checkClosed(); - - try { - beaconClient.sendRequest(req); - return beaconClient.receiveResponse(); - } catch (Exception e) { - throw unwrapException(e); - } - } - - private BeaconException unwrapException(Exception exception) { - if (exception instanceof ServerException s) { - return new BeaconException("An internal server error occurred", s); - } - - if (exception instanceof ClientException s) { - return new BeaconException("A client error occurred", s); - } - - if (exception instanceof ConnectorException s) { - return new BeaconException("A beacon connection error occurred", s); - } - - return new BeaconException("An unexpected error occurred", exception); - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconConnectorException.java b/beacon/src/main/java/io/xpipe/beacon/BeaconConnectorException.java new file mode 100644 index 00000000..afdb86b2 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconConnectorException.java @@ -0,0 +1,25 @@ +package io.xpipe.beacon; + +/** + * Indicates that a connection error occurred. + */ +public class BeaconConnectorException extends Exception { + + public BeaconConnectorException() {} + + public BeaconConnectorException(String message) { + super(message); + } + + public BeaconConnectorException(String message, Throwable cause) { + super(message, cause); + } + + public BeaconConnectorException(Throwable cause) { + super(cause); + } + + public BeaconConnectorException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconException.java b/beacon/src/main/java/io/xpipe/beacon/BeaconException.java deleted file mode 100644 index 5c4bd3b6..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconException.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.xpipe.beacon; - -/** - * An unchecked exception that will be thrown in any case of an underlying exception. - */ -public class BeaconException extends RuntimeException { - - public BeaconException() {} - - public BeaconException(String message) { - super(message); - } - - public BeaconException(String message, Throwable cause) { - super(message, cause); - } - - public BeaconException(Throwable cause) { - super(cause); - } - - public BeaconException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconFormat.java b/beacon/src/main/java/io/xpipe/beacon/BeaconFormat.java deleted file mode 100644 index 029f2305..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconFormat.java +++ /dev/null @@ -1,112 +0,0 @@ -package io.xpipe.beacon; - -import lombok.experimental.UtilityClass; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; - -@UtilityClass -public class BeaconFormat { - - private static final int SEGMENT_SIZE = 65536; - - public static OutputStream writeBlocks(OutputStream out) { - return new OutputStream() { - private final byte[] currentBytes = new byte[SEGMENT_SIZE]; - private int index; - - @Override - public void write(int b) throws IOException { - if (isClosed()) { - throw new IllegalStateException("Output is closed"); - } - - if (index == currentBytes.length) { - finishBlock(); - } - - currentBytes[index] = (byte) b; - index++; - } - - @Override - public void close() throws IOException { - if (isClosed()) { - return; - } - - finishBlock(); - out.flush(); - index = -1; - } - - private boolean isClosed() { - return index == -1; - } - - private void finishBlock() throws IOException { - if (isClosed()) { - throw new IllegalStateException("Output is closed"); - } - - if (BeaconConfig.printMessages()) { - System.out.println("Sending data block of length " + index); - } - - int length = index; - var lengthBuffer = ByteBuffer.allocate(4).putInt(length); - out.write(lengthBuffer.array()); - out.write(currentBytes, 0, length); - index = 0; - } - }; - } - - public static InputStream readBlocks(InputStream in) { - return new InputStream() { - - private byte[] currentBytes; - private int index; - private boolean lastBlock; - - @Override - public int read() throws IOException { - if ((currentBytes == null || index == currentBytes.length) && !lastBlock) { - if (!readBlock()) { - return -1; - } - } - - if (currentBytes != null && index == currentBytes.length && lastBlock) { - return -1; - } - - int out = currentBytes[index] & 0xff; - index++; - return out; - } - - private boolean readBlock() throws IOException { - var length = in.readNBytes(4); - if (length.length < 4) { - return false; - } - - var lengthInt = ByteBuffer.wrap(length).getInt(); - - if (BeaconConfig.printMessages()) { - System.out.println("Receiving data block of length " + lengthInt); - } - - currentBytes = in.readNBytes(lengthInt); - index = 0; - if (lengthInt < SEGMENT_SIZE) { - lastBlock = true; - } - return true; - } - }; - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconHandler.java b/beacon/src/main/java/io/xpipe/beacon/BeaconHandler.java deleted file mode 100644 index 8b4a960b..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconHandler.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.xpipe.beacon; - -import io.xpipe.core.util.FailableRunnable; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -/** - * An exchange handler responsible for properly handling a request and sending a response. - */ -public interface BeaconHandler { - - /** - * Execute a Runnable after the initial response has been sent. - * - * @param r the runnable to execute - */ - void postResponse(FailableRunnable r); - - /** - * Prepares to attach a body to a response. - * - * @return the output stream that can be used to write the body payload - */ - OutputStream sendBody() throws IOException; - - /** - * Prepares to read an attached body of a request. - * - * @return the input stream that can be used to read the body payload - */ - InputStream receiveBody() throws IOException; -} diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconInterface.java b/beacon/src/main/java/io/xpipe/beacon/BeaconInterface.java new file mode 100644 index 00000000..28ed9f7b --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconInterface.java @@ -0,0 +1,74 @@ +package io.xpipe.beacon; + +import com.sun.net.httpserver.HttpExchange; +import io.xpipe.core.util.ModuleLayerLoader; +import lombok.SneakyThrows; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.stream.Collectors; + +public abstract class BeaconInterface { + + private static List> ALL; + + public static List> getAll() { + return ALL; + } + + public static Optional> byPath(String path) { + return ALL.stream() + .filter(d -> d.getPath().equals(path)) + .findAny(); + } + + + public static Optional> byRequest(RQ req) { + return ALL.stream() + .filter(d -> d.getRequestClass().equals(req.getClass())) + .findAny(); + } + + public static class Loader implements ModuleLayerLoader { + + @Override + public void init(ModuleLayer layer) { + var services = layer != null ? ServiceLoader.load(layer, BeaconInterface.class) : ServiceLoader.load(BeaconInterface.class); + ALL = services.stream() + .map(ServiceLoader.Provider::get) + .map(beaconInterface -> (BeaconInterface) beaconInterface) + .collect(Collectors.toList()); + // Remove parent classes + ALL.removeIf(beaconInterface -> ALL.stream().anyMatch(other -> + !other.equals(beaconInterface) && beaconInterface.getClass().isAssignableFrom(other.getClass()))); + } + } + + @SuppressWarnings("unchecked") + @SneakyThrows + public Class getRequestClass() { + var c = getClass().getSuperclass(); + var name = (c.getSuperclass().equals(BeaconInterface.class) ? c : getClass()).getName() + "$Request"; + return (Class) Class.forName(name); + } + + @SuppressWarnings("unchecked") + @SneakyThrows + public Class getResponseClass() { + var c = getClass().getSuperclass(); + var name = (c.getSuperclass().equals(BeaconInterface.class) ? c : getClass()).getName() + "$Response"; + return (Class) Class.forName(name); + } + + public boolean requiresAuthentication() { + return true; + } + + public abstract String getPath(); + + public Object handle(HttpExchange exchange, T body) throws IOException, BeaconClientException, BeaconServerException { + throw new UnsupportedOperationException(); + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconJacksonModule.java b/beacon/src/main/java/io/xpipe/beacon/BeaconJacksonModule.java index 9a8da0e1..f11a945a 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconJacksonModule.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconJacksonModule.java @@ -8,8 +8,11 @@ public class BeaconJacksonModule extends SimpleModule { @Override public void setupModule(SetupContext context) { context.registerSubtypes( - new NamedType(BeaconClient.ApiClientInformation.class), - new NamedType(BeaconClient.CliClientInformation.class), - new NamedType(BeaconClient.DaemonInformation.class)); + new NamedType(BeaconClientInformation.Api.class), + new NamedType(BeaconClientInformation.Cli.class), + new NamedType(BeaconClientInformation.Daemon.class)); + context.registerSubtypes( + new NamedType(BeaconAuthMethod.Local.class), + new NamedType(BeaconAuthMethod.ApiKey.class)); } } diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java b/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java index 50649fad..40cddc25 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java @@ -1,6 +1,6 @@ package io.xpipe.beacon; -import io.xpipe.beacon.exchange.StopExchange; +import io.xpipe.beacon.api.DaemonStopExchange; import io.xpipe.core.process.OsType; import io.xpipe.core.store.FileNames; import io.xpipe.core.util.XPipeDaemonMode; @@ -18,9 +18,9 @@ import java.util.List; */ public class BeaconServer { - public static boolean isReachable() { + public static boolean isReachable(int port) { try (var socket = new Socket()) { - socket.connect(new InetSocketAddress(InetAddress.getLoopbackAddress(), BeaconConfig.getUsedPort()), 5000); + socket.connect(new InetSocketAddress(InetAddress.getLoopbackAddress(), port), 5000); return true; } catch (Exception e) { return false; @@ -108,8 +108,7 @@ public class BeaconServer { } public static boolean tryStop(BeaconClient client) throws Exception { - client.sendRequest(StopExchange.Request.builder().build()); - StopExchange.Response res = client.receiveResponse(); + DaemonStopExchange.Response res = client.performRequest(DaemonStopExchange.Request.builder().build()); return res.isSuccess(); } diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconServerErrorResponse.java b/beacon/src/main/java/io/xpipe/beacon/BeaconServerErrorResponse.java new file mode 100644 index 00000000..28f63450 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconServerErrorResponse.java @@ -0,0 +1,20 @@ +package io.xpipe.beacon; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +@SuppressWarnings("ClassCanBeRecord") +@Value +@Builder +@Jacksonized +@AllArgsConstructor +public class BeaconServerErrorResponse { + + Throwable error; + + public void throwError() throws BeaconServerException { + throw new BeaconServerException(error.getMessage(), error); + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconServerException.java b/beacon/src/main/java/io/xpipe/beacon/BeaconServerException.java new file mode 100644 index 00000000..8247bc0a --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconServerException.java @@ -0,0 +1,23 @@ +package io.xpipe.beacon; + +/** + * Indicates that an internal server error occurred. + */ +public class BeaconServerException extends Exception { + + public BeaconServerException(String message) { + super(message); + } + + public BeaconServerException(String message, Throwable cause) { + super(message, cause); + } + + public BeaconServerException(Throwable cause) { + super(cause); + } + + public BeaconServerException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/ClientException.java b/beacon/src/main/java/io/xpipe/beacon/ClientException.java deleted file mode 100644 index 784d9b64..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/ClientException.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.xpipe.beacon; - -/** - * Indicates that a client request caused an issue. - */ -public class ClientException extends Exception { - - public ClientException() {} - - public ClientException(String message) { - super(message); - } - - public ClientException(String message, Throwable cause) { - super(message, cause); - } - - public ClientException(Throwable cause) { - super(cause); - } - - public ClientException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/ConnectorException.java b/beacon/src/main/java/io/xpipe/beacon/ConnectorException.java deleted file mode 100644 index 83a14dbc..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/ConnectorException.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.xpipe.beacon; - -/** - * Indicates that a connection error occurred. - */ -public class ConnectorException extends Exception { - - public ConnectorException() {} - - public ConnectorException(String message) { - super(message); - } - - public ConnectorException(String message, Throwable cause) { - super(message, cause); - } - - public ConnectorException(Throwable cause) { - super(cause); - } - - public ConnectorException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/RequestMessage.java b/beacon/src/main/java/io/xpipe/beacon/RequestMessage.java deleted file mode 100644 index 9f9a5c2d..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/RequestMessage.java +++ /dev/null @@ -1,3 +0,0 @@ -package io.xpipe.beacon; - -public interface RequestMessage {} diff --git a/beacon/src/main/java/io/xpipe/beacon/ResponseMessage.java b/beacon/src/main/java/io/xpipe/beacon/ResponseMessage.java deleted file mode 100644 index ec167994..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/ResponseMessage.java +++ /dev/null @@ -1,3 +0,0 @@ -package io.xpipe.beacon; - -public interface ResponseMessage {} diff --git a/beacon/src/main/java/io/xpipe/beacon/ServerException.java b/beacon/src/main/java/io/xpipe/beacon/ServerException.java deleted file mode 100644 index d25c2272..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/ServerException.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.xpipe.beacon; - -/** - * Indicates that an internal server error occurred. - */ -public class ServerException extends Exception { - - public ServerException() {} - - public ServerException(String message) { - super(message); - } - - public ServerException(String message, Throwable cause) { - super(message, cause); - } - - public ServerException(Throwable cause) { - super(cause); - } - - public ServerException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/api/AskpassExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/AskpassExchange.java new file mode 100644 index 00000000..c77f5057 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/api/AskpassExchange.java @@ -0,0 +1,35 @@ +package io.xpipe.beacon.api; + +import io.xpipe.beacon.BeaconInterface; +import io.xpipe.core.util.SecretValue; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.UUID; + +public class AskpassExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/askpass"; + } + + @Jacksonized + @Builder + @Value + public static class Request { + UUID secretId; + + UUID request; + + String prompt; + } + + @Jacksonized + @Builder + @Value + public static class Response { + SecretValue value; + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ConnectionQueryExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionQueryExchange.java new file mode 100644 index 00000000..829d41fa --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/api/ConnectionQueryExchange.java @@ -0,0 +1,53 @@ +package io.xpipe.beacon.api; + +import io.xpipe.beacon.BeaconInterface; +import io.xpipe.core.store.StorePath; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; +import java.util.UUID; + +public class ConnectionQueryExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/connection/query"; + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + String categoryFilter; + @NonNull + String connectionFilter; + @NonNull + String typeFilter; + } + + @Jacksonized + @Builder + @Value + public static class Response { + @NonNull + List found; + } + + @Jacksonized + @Builder + @Value + public static class QueryResponse { + @NonNull + UUID uuid; + @NonNull + StorePath category; + @NonNull + StorePath connection; + @NonNull + String type; + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/api/DaemonFocusExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/DaemonFocusExchange.java new file mode 100644 index 00000000..3a4bc002 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/api/DaemonFocusExchange.java @@ -0,0 +1,29 @@ +package io.xpipe.beacon.api; + +import io.xpipe.beacon.BeaconInterface; +import io.xpipe.core.util.XPipeDaemonMode; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +public class DaemonFocusExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/daemon/focus"; + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + XPipeDaemonMode mode; + } + + @Jacksonized + @Builder + @Value + public static class Response {} +} diff --git a/beacon/src/main/java/io/xpipe/beacon/api/DaemonModeExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/DaemonModeExchange.java new file mode 100644 index 00000000..b9bcf2fd --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/api/DaemonModeExchange.java @@ -0,0 +1,32 @@ +package io.xpipe.beacon.api; + +import io.xpipe.beacon.BeaconInterface; +import io.xpipe.core.util.XPipeDaemonMode; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +public class DaemonModeExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/daemon/mode"; + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + XPipeDaemonMode mode; + } + + @Jacksonized + @Builder + @Value + public static class Response { + @NonNull + XPipeDaemonMode usedMode; + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/api/DaemonOpenExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/DaemonOpenExchange.java new file mode 100644 index 00000000..1fe22076 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/api/DaemonOpenExchange.java @@ -0,0 +1,30 @@ +package io.xpipe.beacon.api; + +import io.xpipe.beacon.BeaconInterface; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; + +public class DaemonOpenExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/daemon/open"; + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + List arguments; + } + + @Jacksonized + @Builder + @Value + public static class Response {} +} diff --git a/beacon/src/main/java/io/xpipe/beacon/api/DaemonStatusExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/DaemonStatusExchange.java new file mode 100644 index 00000000..379e7b0e --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/api/DaemonStatusExchange.java @@ -0,0 +1,27 @@ +package io.xpipe.beacon.api; + +import io.xpipe.beacon.BeaconInterface; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +public class DaemonStatusExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/daemon/status"; + } + + @Value + @Jacksonized + @Builder + public static class Request { + } + + @Jacksonized + @Builder + @Value + public static class Response { + String mode; + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/api/DaemonStopExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/DaemonStopExchange.java new file mode 100644 index 00000000..5ae633f3 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/api/DaemonStopExchange.java @@ -0,0 +1,30 @@ +package io.xpipe.beacon.api; + +import io.xpipe.beacon.BeaconInterface; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +/** + * Requests the daemon to stop. + */ +public class DaemonStopExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/daemon/stop"; + } + + @Jacksonized + @Builder + @Value + public static class Request { + } + + @Jacksonized + @Builder + @Value + public static class Response { + boolean success; + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/api/DaemonVersionExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/DaemonVersionExchange.java new file mode 100644 index 00000000..18c107eb --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/api/DaemonVersionExchange.java @@ -0,0 +1,30 @@ +package io.xpipe.beacon.api; + +import io.xpipe.beacon.BeaconInterface; +import lombok.Builder; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +public class DaemonVersionExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/daemon/version"; + } + + @Jacksonized + @Builder + @Value + public static class Request { + } + + @Jacksonized + @Builder + @Value + public static class Response { + + String version; + String buildVersion; + String jvmVersion; + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/api/HandshakeExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/HandshakeExchange.java new file mode 100644 index 00000000..bab3caf5 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/api/HandshakeExchange.java @@ -0,0 +1,40 @@ +package io.xpipe.beacon.api; + +import io.xpipe.beacon.BeaconAuthMethod; +import io.xpipe.beacon.BeaconClientInformation; +import io.xpipe.beacon.BeaconInterface; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +public class HandshakeExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/handshake"; + } + + @Override + public boolean requiresAuthentication() { + return false; + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + BeaconAuthMethod auth; + @NonNull + BeaconClientInformation client; + } + + @Jacksonized + @Builder + @Value + public static class Response { + @NonNull + String sessionToken; + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ShellExecExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/ShellExecExchange.java new file mode 100644 index 00000000..3ad47bcb --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/api/ShellExecExchange.java @@ -0,0 +1,38 @@ +package io.xpipe.beacon.api; + +import io.xpipe.beacon.BeaconInterface; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.UUID; + +public class ShellExecExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/shell/exec"; + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + UUID connection; + @NonNull + String command; + } + + @Jacksonized + @Builder + @Value + public static class Response { + long exitCode; + @NonNull + String stdout; + @NonNull + String stderr; + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ShellStartExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/ShellStartExchange.java new file mode 100644 index 00000000..0bb995cc --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/api/ShellStartExchange.java @@ -0,0 +1,30 @@ +package io.xpipe.beacon.api; + +import io.xpipe.beacon.BeaconInterface; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.UUID; + +public class ShellStartExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/shell/start"; + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + UUID connection; + } + + @Jacksonized + @Builder + @Value + public static class Response {} +} diff --git a/beacon/src/main/java/io/xpipe/beacon/api/ShellStopExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/ShellStopExchange.java new file mode 100644 index 00000000..3c253265 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/api/ShellStopExchange.java @@ -0,0 +1,30 @@ +package io.xpipe.beacon.api; + +import io.xpipe.beacon.BeaconInterface; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.UUID; + +public class ShellStopExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/shell/stop"; + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + UUID connection; + } + + @Jacksonized + @Builder + @Value + public static class Response {} +} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/TerminalLaunchExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/TerminalLaunchExchange.java similarity index 50% rename from beacon/src/main/java/io/xpipe/beacon/exchange/TerminalLaunchExchange.java rename to beacon/src/main/java/io/xpipe/beacon/api/TerminalLaunchExchange.java index 6f314a64..82d53dbd 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/TerminalLaunchExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/api/TerminalLaunchExchange.java @@ -1,8 +1,6 @@ -package io.xpipe.beacon.exchange; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; +package io.xpipe.beacon.api; +import io.xpipe.beacon.BeaconInterface; import lombok.Builder; import lombok.NonNull; import lombok.Value; @@ -11,17 +9,17 @@ import lombok.extern.jackson.Jacksonized; import java.nio.file.Path; import java.util.UUID; -public class TerminalLaunchExchange implements MessageExchange { +public class TerminalLaunchExchange extends BeaconInterface { @Override - public String getId() { - return "terminalLaunch"; + public String getPath() { + return "/terminalLaunch"; } @Jacksonized @Builder @Value - public static class Request implements RequestMessage { + public static class Request { @NonNull UUID request; } @@ -29,7 +27,7 @@ public class TerminalLaunchExchange implements MessageExchange { @Jacksonized @Builder @Value - public static class Response implements ResponseMessage { + public static class Response { @NonNull Path targetFile; } diff --git a/beacon/src/main/java/io/xpipe/beacon/api/TerminalWaitExchange.java b/beacon/src/main/java/io/xpipe/beacon/api/TerminalWaitExchange.java new file mode 100644 index 00000000..da4d91d6 --- /dev/null +++ b/beacon/src/main/java/io/xpipe/beacon/api/TerminalWaitExchange.java @@ -0,0 +1,30 @@ +package io.xpipe.beacon.api; + +import io.xpipe.beacon.BeaconInterface; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import lombok.extern.jackson.Jacksonized; + +import java.util.UUID; + +public class TerminalWaitExchange extends BeaconInterface { + + @Override + public String getPath() { + return "/terminalWait"; + } + + @Jacksonized + @Builder + @Value + public static class Request { + @NonNull + UUID request; + } + + @Jacksonized + @Builder + @Value + public static class Response {} +} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/AskpassExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/AskpassExchange.java deleted file mode 100644 index 8a1be7ca..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/AskpassExchange.java +++ /dev/null @@ -1,39 +0,0 @@ -package io.xpipe.beacon.exchange; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.core.util.SecretValue; - -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -import java.util.UUID; - -public class AskpassExchange implements MessageExchange { - - @Override - public String getId() { - return "askpass"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage { - UUID secretId; - - @NonNull - UUID request; - - String prompt; - } - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage { - SecretValue value; - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/DrainExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/DrainExchange.java deleted file mode 100644 index 573b2ad6..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/DrainExchange.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.xpipe.beacon.exchange; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.core.store.DataStoreId; - -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -public class DrainExchange implements MessageExchange { - - @Override - public String getId() { - return "drain"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage { - @NonNull - DataStoreId source; - - @NonNull - String path; - } - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage {} -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/FocusExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/FocusExchange.java deleted file mode 100644 index f9082bda..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/FocusExchange.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.xpipe.beacon.exchange; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.core.util.XPipeDaemonMode; - -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -public class FocusExchange implements MessageExchange { - - @Override - public String getId() { - return "focus"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage { - @NonNull - XPipeDaemonMode mode; - } - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage {} -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/LaunchExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/LaunchExchange.java deleted file mode 100644 index 2b1e03cc..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/LaunchExchange.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.xpipe.beacon.exchange; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.core.store.DataStoreId; - -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -import java.util.List; - -public class LaunchExchange implements MessageExchange { - - @Override - public String getId() { - return "launch"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage { - @NonNull - DataStoreId id; - } - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage { - @NonNull - List command; - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/MessageExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/MessageExchange.java deleted file mode 100644 index 999cd705..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/MessageExchange.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.xpipe.beacon.exchange; - -import lombok.SneakyThrows; - -/** - * A message exchange scheme that implements a certain functionality. - */ -public interface MessageExchange { - - /** - * The unique id of this exchange that will be included in the messages. - */ - String getId(); - - /** - * Returns the request class, needed for serialization. - */ - @SneakyThrows - default Class getRequestClass() { - var c = getClass().getSuperclass(); - var name = (MessageExchange.class.isAssignableFrom(c) ? c : getClass()).getName() + "$Request"; - return Class.forName(name); - } - - /** - * Returns the response class, needed for serialization. - */ - @SneakyThrows - default Class getResponseClass() { - var c = getClass().getSuperclass(); - var name = (MessageExchange.class.isAssignableFrom(c) ? c : getClass()).getName() + "$Response"; - return Class.forName(name); - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/MessageExchanges.java b/beacon/src/main/java/io/xpipe/beacon/exchange/MessageExchanges.java deleted file mode 100644 index 54c0db98..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/MessageExchanges.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.xpipe.beacon.exchange; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; - -import java.util.List; -import java.util.Optional; -import java.util.ServiceLoader; -import java.util.stream.Collectors; - -public class MessageExchanges { - - private static List ALL; - - public static void loadAll() { - if (ALL == null) { - ALL = ServiceLoader.load(MessageExchange.class).stream() - .map(s -> { - return s.get(); - }) - .collect(Collectors.toList()); - } - } - - public static Optional byId(String name) { - loadAll(); - return ALL.stream().filter(d -> d.getId().equals(name)).findAny(); - } - - public static Optional byRequest(RQ req) { - loadAll(); - return ALL.stream() - .filter(d -> d.getRequestClass().equals(req.getClass())) - .findAny(); - } - - public static Optional byResponse(RP rep) { - loadAll(); - return ALL.stream() - .filter(d -> d.getResponseClass().equals(rep.getClass())) - .findAny(); - } - - public static List getAll() { - loadAll(); - return ALL; - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/OpenExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/OpenExchange.java deleted file mode 100644 index 29533f64..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/OpenExchange.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.xpipe.beacon.exchange; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; - -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -import java.util.List; - -public class OpenExchange implements MessageExchange { - - @Override - public String getId() { - return "open"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage { - @NonNull - List arguments; - } - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage {} -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/QueryStoreExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/QueryStoreExchange.java deleted file mode 100644 index e656a72e..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/QueryStoreExchange.java +++ /dev/null @@ -1,51 +0,0 @@ -package io.xpipe.beacon.exchange; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.core.store.DataStore; - -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -import java.util.LinkedHashMap; - -/** - * Queries general information about a data source. - */ -public class QueryStoreExchange implements MessageExchange { - - @Override - public String getId() { - return "queryStore"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage { - @NonNull - String name; - } - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage { - @NonNull - String name; - - String information; - - String summary; - - @NonNull - String provider; - - @NonNull - LinkedHashMap config; - - DataStore internalStore; - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/SinkExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/SinkExchange.java deleted file mode 100644 index 8f5f308e..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/SinkExchange.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.xpipe.beacon.exchange; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.core.store.DataStoreId; - -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -public class SinkExchange implements MessageExchange { - - @Override - public String getId() { - return "sink"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage { - @NonNull - DataStoreId source; - - @NonNull - String path; - } - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage {} -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/StopExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/StopExchange.java deleted file mode 100644 index 6ee1475a..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/StopExchange.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.xpipe.beacon.exchange; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; - -import lombok.Builder; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -/** - * Requests the daemon to stop. - */ -public class StopExchange implements MessageExchange { - - @Override - public String getId() { - return "stop"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage {} - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage { - boolean success; - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/TerminalWaitExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/TerminalWaitExchange.java deleted file mode 100644 index a0cf98f9..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/TerminalWaitExchange.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.xpipe.beacon.exchange; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; - -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -import java.util.UUID; - -public class TerminalWaitExchange implements MessageExchange { - - @Override - public String getId() { - return "terminalWait"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage { - @NonNull - UUID request; - } - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage {} -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/DialogExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/DialogExchange.java deleted file mode 100644 index dfa50dbc..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/DialogExchange.java +++ /dev/null @@ -1,50 +0,0 @@ -package io.xpipe.beacon.exchange.cli; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.beacon.exchange.MessageExchange; -import io.xpipe.core.dialog.DialogElement; - -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -import java.util.UUID; - -public class DialogExchange implements MessageExchange { - - @Override - public String getId() { - return "dialog"; - } - - @Override - public Class getRequestClass() { - return DialogExchange.Request.class; - } - - @Override - public Class getResponseClass() { - return DialogExchange.Response.class; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage { - @NonNull - UUID dialogKey; - - String value; - boolean cancel; - } - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage { - DialogElement element; - String errorMsg; - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/EditStoreExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/EditStoreExchange.java deleted file mode 100644 index 351dd69e..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/EditStoreExchange.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.xpipe.beacon.exchange.cli; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.beacon.exchange.MessageExchange; -import io.xpipe.core.dialog.DialogReference; - -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -public class EditStoreExchange implements MessageExchange { - - @Override - public String getId() { - return "editEntry"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage { - @NonNull - String name; - } - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage { - @NonNull - DialogReference dialog; - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/ListCollectionsExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/ListCollectionsExchange.java deleted file mode 100644 index 270f0447..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/ListCollectionsExchange.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.xpipe.beacon.exchange.cli; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.beacon.exchange.MessageExchange; -import io.xpipe.beacon.exchange.data.CollectionListEntry; - -import lombok.Builder; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -import java.util.List; - -public class ListCollectionsExchange implements MessageExchange { - - @Override - public String getId() { - return "listCollections"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage {} - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage { - List entries; - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/ListEntriesExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/ListEntriesExchange.java deleted file mode 100644 index f7a0a577..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/ListEntriesExchange.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.xpipe.beacon.exchange.cli; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.beacon.exchange.MessageExchange; -import io.xpipe.beacon.exchange.data.EntryListEntry; - -import lombok.Builder; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -import java.util.List; - -public class ListEntriesExchange implements MessageExchange { - - @Override - public String getId() { - return "listEntries"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage { - String collection; - } - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage { - List entries; - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/ListStoresExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/ListStoresExchange.java deleted file mode 100644 index cb5377d6..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/ListStoresExchange.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.xpipe.beacon.exchange.cli; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.beacon.exchange.MessageExchange; -import io.xpipe.beacon.exchange.data.StoreListEntry; - -import lombok.Builder; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -import java.util.List; - -public class ListStoresExchange implements MessageExchange { - - @Override - public String getId() { - return "listStores"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage {} - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage { - List entries; - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/ModeExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/ModeExchange.java deleted file mode 100644 index b9ad4cfd..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/ModeExchange.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.xpipe.beacon.exchange.cli; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.beacon.exchange.MessageExchange; -import io.xpipe.core.util.XPipeDaemonMode; - -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -public class ModeExchange implements MessageExchange { - - @Override - public String getId() { - return "mode"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage { - @NonNull - XPipeDaemonMode mode; - } - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage { - @NonNull - XPipeDaemonMode usedMode; - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/ReadDrainExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/ReadDrainExchange.java deleted file mode 100644 index 1cc2bd2c..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/ReadDrainExchange.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.xpipe.beacon.exchange.cli; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.beacon.exchange.MessageExchange; - -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -public class ReadDrainExchange implements MessageExchange { - - @Override - public String getId() { - return "readDrain"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage { - @NonNull - String name; - } - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage {} -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/RemoveCollectionExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/RemoveCollectionExchange.java deleted file mode 100644 index bde33574..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/RemoveCollectionExchange.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.xpipe.beacon.exchange.cli; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.beacon.exchange.MessageExchange; - -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -public class RemoveCollectionExchange implements MessageExchange { - - @Override - public String getId() { - return "removeCollection"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage { - @NonNull - String collectionName; - } - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage {} -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/RemoveStoreExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/RemoveStoreExchange.java deleted file mode 100644 index 0c6dacf5..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/RemoveStoreExchange.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.xpipe.beacon.exchange.cli; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.beacon.exchange.MessageExchange; - -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -public class RemoveStoreExchange implements MessageExchange { - - @Override - public String getId() { - return "removeStore"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage { - @NonNull - String storeName; - } - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage {} -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/RenameCollectionExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/RenameCollectionExchange.java deleted file mode 100644 index 693179a7..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/RenameCollectionExchange.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.xpipe.beacon.exchange.cli; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.beacon.exchange.MessageExchange; - -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -public class RenameCollectionExchange implements MessageExchange { - - @Override - public String getId() { - return "renameCollection"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage { - @NonNull - String collectionName; - - @NonNull - String newName; - } - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage {} -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/RenameStoreExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/RenameStoreExchange.java deleted file mode 100644 index f7c9a98f..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/RenameStoreExchange.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.xpipe.beacon.exchange.cli; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.beacon.exchange.MessageExchange; - -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -public class RenameStoreExchange implements MessageExchange { - - @Override - public String getId() { - return "renameStore"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage { - @NonNull - String storeName; - - @NonNull - String newName; - } - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage {} -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/StatusExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/StatusExchange.java deleted file mode 100644 index 7d08abae..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/StatusExchange.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.xpipe.beacon.exchange.cli; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.beacon.exchange.MessageExchange; - -import lombok.Builder; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -public class StatusExchange implements MessageExchange { - - @Override - public String getId() { - return "status"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage {} - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage { - String mode; - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/StoreAddExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/StoreAddExchange.java deleted file mode 100644 index 898a591b..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/StoreAddExchange.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.xpipe.beacon.exchange.cli; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.beacon.exchange.MessageExchange; -import io.xpipe.core.dialog.DialogReference; -import io.xpipe.core.store.DataStore; - -import lombok.Builder; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -public class StoreAddExchange implements MessageExchange { - - @Override - public String getId() { - return "storeAdd"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage { - DataStore storeInput; - - String type; - String name; - } - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage { - DialogReference config; - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/StoreProviderListExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/StoreProviderListExchange.java deleted file mode 100644 index d7a173e9..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/StoreProviderListExchange.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.xpipe.beacon.exchange.cli; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.beacon.exchange.MessageExchange; -import io.xpipe.beacon.exchange.data.ProviderEntry; - -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -import java.util.List; -import java.util.Map; - -public class StoreProviderListExchange implements MessageExchange { - - @Override - public String getId() { - return "storeProviderList"; - } - - @Jacksonized - @Builder - @Value - public static class Request implements RequestMessage {} - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage { - @NonNull - Map> entries; - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/VersionExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/VersionExchange.java deleted file mode 100644 index 14155cd7..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/VersionExchange.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.xpipe.beacon.exchange.cli; - -import io.xpipe.beacon.RequestMessage; -import io.xpipe.beacon.ResponseMessage; -import io.xpipe.beacon.exchange.MessageExchange; - -import lombok.Builder; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -public class VersionExchange implements MessageExchange { - - @Override - public String getId() { - return "version"; - } - - @lombok.extern.jackson.Jacksonized - @lombok.Builder - @lombok.Value - public static class Request implements RequestMessage {} - - @Jacksonized - @Builder - @Value - public static class Response implements ResponseMessage { - - String version; - String buildVersion; - String jvmVersion; - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/data/CollectionListEntry.java b/beacon/src/main/java/io/xpipe/beacon/exchange/data/CollectionListEntry.java deleted file mode 100644 index 93e80284..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/data/CollectionListEntry.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.xpipe.beacon.exchange.data; - -import lombok.Builder; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -import java.time.Instant; - -@Value -@Jacksonized -@Builder -public class CollectionListEntry { - - String name; - int size; - Instant lastUsed; -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/data/EntryListEntry.java b/beacon/src/main/java/io/xpipe/beacon/exchange/data/EntryListEntry.java deleted file mode 100644 index 0df3fdb7..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/data/EntryListEntry.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.xpipe.beacon.exchange.data; - -import lombok.Builder; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -import java.time.Instant; - -@Value -@Jacksonized -@Builder -public class EntryListEntry { - String name; - String type; - String description; - Instant lastUsed; -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/data/ProviderEntry.java b/beacon/src/main/java/io/xpipe/beacon/exchange/data/ProviderEntry.java deleted file mode 100644 index 4b67c194..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/data/ProviderEntry.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.xpipe.beacon.exchange.data; - -import lombok.Builder; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -@Value -@Jacksonized -@Builder -public class ProviderEntry { - String id; - String description; - boolean hidden; -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/data/ServerErrorMessage.java b/beacon/src/main/java/io/xpipe/beacon/exchange/data/ServerErrorMessage.java deleted file mode 100644 index 5e0e6e20..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/data/ServerErrorMessage.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.xpipe.beacon.exchange.data; - -import io.xpipe.beacon.ServerException; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -import java.util.UUID; - -@SuppressWarnings("ClassCanBeRecord") -@Value -@Builder -@Jacksonized -@AllArgsConstructor -public class ServerErrorMessage { - - UUID requestId; - Throwable error; - - public void throwError() throws ServerException { - throw new ServerException(error.getMessage(), error); - } -} diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/data/StoreListEntry.java b/beacon/src/main/java/io/xpipe/beacon/exchange/data/StoreListEntry.java deleted file mode 100644 index 1853245e..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/data/StoreListEntry.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.xpipe.beacon.exchange.data; - -import io.xpipe.core.store.DataStoreId; - -import lombok.Builder; -import lombok.Value; -import lombok.extern.jackson.Jacksonized; - -@Value -@Jacksonized -@Builder -public class StoreListEntry { - - DataStoreId id; - String type; - String information; -} diff --git a/beacon/src/main/java/io/xpipe/beacon/test/BeaconDaemonController.java b/beacon/src/main/java/io/xpipe/beacon/test/BeaconDaemonController.java index a5569604..1617d34c 100644 --- a/beacon/src/main/java/io/xpipe/beacon/test/BeaconDaemonController.java +++ b/beacon/src/main/java/io/xpipe/beacon/test/BeaconDaemonController.java @@ -1,6 +1,8 @@ package io.xpipe.beacon.test; import io.xpipe.beacon.BeaconClient; +import io.xpipe.beacon.BeaconClientInformation; +import io.xpipe.beacon.BeaconConfig; import io.xpipe.beacon.BeaconServer; import io.xpipe.core.util.XPipeDaemonMode; import io.xpipe.core.util.XPipeInstallation; @@ -12,7 +14,7 @@ public class BeaconDaemonController { private static boolean alreadyStarted; public static void start(XPipeDaemonMode mode) throws Exception { - if (BeaconServer.isReachable()) { + if (BeaconServer.isReachable(BeaconConfig.getUsedPort())) { alreadyStarted = true; return; } @@ -27,7 +29,7 @@ public class BeaconDaemonController { } waitForStartup(process, custom); - if (!BeaconServer.isReachable()) { + if (!BeaconServer.isReachable(BeaconConfig.getUsedPort())) { throw new AssertionError(); } } @@ -37,13 +39,12 @@ public class BeaconDaemonController { return; } - if (!BeaconServer.isReachable()) { + if (!BeaconServer.isReachable(BeaconConfig.getUsedPort())) { return; } - var client = BeaconClient.establishConnection(BeaconClient.ApiClientInformation.builder() - .version("?") - .language("Java API Test") + var client = BeaconClient.establishConnection(BeaconConfig.getUsedPort(), BeaconClientInformation.Api.builder() + .name("Beacon daemon controller") .build()); if (!BeaconServer.tryStop(client)) { throw new AssertionError(); @@ -67,9 +68,8 @@ public class BeaconDaemonController { } catch (InterruptedException ignored) { } - var s = BeaconClient.tryEstablishConnection(BeaconClient.ApiClientInformation.builder() - .version("?") - .language("Java") + var s = BeaconClient.tryEstablishConnection(BeaconConfig.getUsedPort(), BeaconClientInformation.Api.builder() + .name("Beacon daemon controller") .build()); if (s.isPresent()) { return; @@ -86,7 +86,7 @@ public class BeaconDaemonController { } catch (InterruptedException ignored) { } - var r = BeaconServer.isReachable(); + var r = BeaconServer.isReachable(BeaconConfig.getUsedPort()); if (!r) { return; } diff --git a/beacon/src/main/java/io/xpipe/beacon/test/BeaconDaemonExtensionTest.java b/beacon/src/main/java/io/xpipe/beacon/test/BeaconDaemonExtensionTest.java index 9e1e1cdb..a66c469c 100644 --- a/beacon/src/main/java/io/xpipe/beacon/test/BeaconDaemonExtensionTest.java +++ b/beacon/src/main/java/io/xpipe/beacon/test/BeaconDaemonExtensionTest.java @@ -1,9 +1,8 @@ package io.xpipe.beacon.test; import io.xpipe.core.process.OsType; -import io.xpipe.core.util.JacksonMapper; +import io.xpipe.core.util.ModuleLayerLoader; import io.xpipe.core.util.XPipeDaemonMode; - import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -11,7 +10,7 @@ public class BeaconDaemonExtensionTest { @BeforeAll public static void setup() throws Exception { - JacksonMapper.initModularized(ModuleLayer.boot()); + ModuleLayerLoader.loadAll(ModuleLayer.boot(),throwable -> throwable.printStackTrace()); BeaconDaemonController.start( OsType.getLocal().equals(OsType.WINDOWS) ? XPipeDaemonMode.TRAY : XPipeDaemonMode.BACKGROUND); } diff --git a/beacon/src/main/java/io/xpipe/beacon/util/QuietDialogHandler.java b/beacon/src/main/java/io/xpipe/beacon/util/QuietDialogHandler.java deleted file mode 100644 index d77b0720..00000000 --- a/beacon/src/main/java/io/xpipe/beacon/util/QuietDialogHandler.java +++ /dev/null @@ -1,74 +0,0 @@ -package io.xpipe.beacon.util; - -import io.xpipe.beacon.BeaconConnection; -import io.xpipe.beacon.BeaconException; -import io.xpipe.beacon.exchange.cli.DialogExchange; -import io.xpipe.core.dialog.BaseQueryElement; -import io.xpipe.core.dialog.ChoiceElement; -import io.xpipe.core.dialog.DialogElement; -import io.xpipe.core.dialog.DialogReference; - -import java.util.Map; -import java.util.UUID; - -public class QuietDialogHandler { - - private final UUID dialogKey; - private final BeaconConnection connection; - private final Map overrides; - private DialogElement element; - - public QuietDialogHandler(DialogReference ref, BeaconConnection connection, Map overrides) { - this.dialogKey = ref.getDialogId(); - this.element = ref.getStart(); - this.connection = connection; - this.overrides = overrides; - } - - public static void handle(DialogReference ref, BeaconConnection connection) { - new QuietDialogHandler(ref, connection, Map.of()).handle(); - } - - public void handle() { - String response = null; - - if (element instanceof ChoiceElement c) { - response = handleChoice(c); - } - - if (element instanceof BaseQueryElement q) { - response = handleQuery(q); - } - - DialogExchange.Response res = connection.performSimpleExchange(DialogExchange.Request.builder() - .dialogKey(dialogKey) - .value(response) - .build()); - if (res.getElement() != null && element.equals(res.getElement())) { - throw new BeaconException( - "Invalid value for key " + res.getElement().toDisplayString()); - } - - element = res.getElement(); - - if (element != null) { - handle(); - } - } - - private String handleQuery(BaseQueryElement q) { - if (q.isRequired() && !overrides.containsKey(q.getDescription())) { - throw new IllegalStateException("Missing required config parameter: " + q.getDescription()); - } - - return overrides.get(q.getDescription()); - } - - private String handleChoice(ChoiceElement c) { - if (c.isRequired() && !overrides.containsKey(c.getDescription())) { - throw new IllegalStateException("Missing required config parameter: " + c.getDescription()); - } - - return overrides.get(c.getDescription()); - } -} diff --git a/beacon/src/main/java/module-info.java b/beacon/src/main/java/module-info.java index 64ed1985..be21119d 100644 --- a/beacon/src/main/java/module-info.java +++ b/beacon/src/main/java/module-info.java @@ -1,49 +1,32 @@ -import io.xpipe.beacon.BeaconJacksonModule; -import io.xpipe.beacon.exchange.*; -import io.xpipe.beacon.exchange.cli.*; -import io.xpipe.core.util.ProxyFunction; - import com.fasterxml.jackson.databind.Module; +import io.xpipe.beacon.BeaconInterface; +import io.xpipe.beacon.BeaconJacksonModule; +import io.xpipe.beacon.api.*; +import io.xpipe.core.util.ModuleLayerLoader; open module io.xpipe.beacon { exports io.xpipe.beacon; - exports io.xpipe.beacon.exchange; - exports io.xpipe.beacon.exchange.data; - exports io.xpipe.beacon.exchange.cli; - exports io.xpipe.beacon.util; exports io.xpipe.beacon.test; + exports io.xpipe.beacon.api; - requires static com.fasterxml.jackson.core; - requires static com.fasterxml.jackson.databind; + requires com.fasterxml.jackson.core; + requires com.fasterxml.jackson.annotation; + requires com.fasterxml.jackson.databind; requires transitive io.xpipe.core; requires static lombok; requires static org.junit.jupiter.api; + requires jdk.httpserver; + requires java.net.http; + requires java.desktop; - uses MessageExchange; - uses ProxyFunction; + uses io.xpipe.beacon.BeaconInterface; + provides ModuleLayerLoader with + BeaconInterface.Loader; provides Module with BeaconJacksonModule; - provides io.xpipe.beacon.exchange.MessageExchange with - SinkExchange, - DrainExchange, - LaunchExchange, - EditStoreExchange, - StoreProviderListExchange, - ModeExchange, - QueryStoreExchange, - StatusExchange, - FocusExchange, - OpenExchange, - StopExchange, - RenameStoreExchange, - RemoveStoreExchange, - StoreAddExchange, - ReadDrainExchange, + provides BeaconInterface with ShellStartExchange, ShellStopExchange, ShellExecExchange, DaemonModeExchange, DaemonStatusExchange, DaemonFocusExchange, DaemonOpenExchange, DaemonStopExchange, HandshakeExchange, ConnectionQueryExchange, AskpassExchange, TerminalWaitExchange, - TerminalLaunchExchange, - ListStoresExchange, - DialogExchange, - VersionExchange; + TerminalLaunchExchange, DaemonVersionExchange; } diff --git a/beacon/src/main/resources/META-INF/services/io.xpipe.core.util.ModuleLayerLoader b/beacon/src/main/resources/META-INF/services/io.xpipe.core.util.ModuleLayerLoader new file mode 100644 index 00000000..c70568c8 --- /dev/null +++ b/beacon/src/main/resources/META-INF/services/io.xpipe.core.util.ModuleLayerLoader @@ -0,0 +1 @@ +io.xpipe.beacon.BeaconInterface$Loader \ No newline at end of file diff --git a/build.gradle b/build.gradle index f94acf1c..2e269057 100644 --- a/build.gradle +++ b/build.gradle @@ -88,8 +88,7 @@ project.ext { arch = getArchName() privateExtensions = file("$rootDir/private_extensions.txt").exists() ? file("$rootDir/private_extensions.txt").readLines() : [] isFullRelease = System.getenv('RELEASE') != null && Boolean.parseBoolean(System.getenv('RELEASE')) - isPreRelease = System.getenv('PRERELEASE') != null && Boolean.parseBoolean(System.getenv('PRERELEASE')) - isStage = System.getenv('STAGE') != null && Boolean.parseBoolean(System.getenv('STAGE')) + isStage = true rawVersion = file('version').text.trim() versionString = rawVersion + (isFullRelease || isStage ? '' : '-SNAPSHOT') versionReleaseNumber = rawVersion.split('-').length == 2 ? Integer.parseInt(rawVersion.split('-')[1]) : 1 @@ -106,7 +105,7 @@ project.ext { website = 'https://xpipe.io' sourceWebsite = isStage ? 'https://github.com/xpipe-io/xpipe-ptb' : 'https://github.com/xpipe-io/xpipe' authors = 'Christopher Schnick' - javafxVersion = '22.0.1' + javafxVersion = '23-ea+18' platformName = getPlatformName() languages = ["en", "nl", "es", "fr", "de", "it", "pt", "ru", "ja", "zh", "tr", "da"] jvmRunArgs = [ @@ -159,6 +158,11 @@ if (isFullRelease && rawVersion.contains("-")) { throw new IllegalArgumentException("Releases must have canonical versions") } + +if (isStage && !rawVersion.contains("-")) { + throw new IllegalArgumentException("Stage releases must have release numbers") +} + def replaceVariablesInFileAsString(String f, Map replacements) { def fileName = file(f).getName() def text = file(f).text diff --git a/core/build.gradle b/core/build.gradle index db7993e7..df895257 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -14,9 +14,7 @@ compileJava { dependencies { api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.17.1" - implementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: "2.17.1" implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.17.1" - implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jdk8', version: "2.17.1" } version = rootProject.versionString diff --git a/core/src/main/java/io/xpipe/core/process/CommandFeedbackPredicate.java b/core/src/main/java/io/xpipe/core/process/CommandFeedbackPredicate.java new file mode 100644 index 00000000..10a80005 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/process/CommandFeedbackPredicate.java @@ -0,0 +1,6 @@ +package io.xpipe.core.process; + +public interface CommandFeedbackPredicate { + + boolean test(CommandBuilder command) throws Exception; +} 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 30765336..3e2e81dc 100644 --- a/core/src/main/java/io/xpipe/core/process/OsType.java +++ b/core/src/main/java/io/xpipe/core/process/OsType.java @@ -89,7 +89,17 @@ public interface OsType { @Override public String getTempDirectory(ShellControl pc) throws Exception { - return pc.executeSimpleStringCommand(pc.getShellDialect().getPrintEnvironmentVariableCommand("TEMP")); + 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 @@ -116,7 +126,7 @@ public interface OsType { .trim(); } catch (Throwable t) { // Just in case this fails somehow - return "Windows ?"; + return "Windows"; } } } 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 c17fb2b8..3110814b 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellDialects.java +++ b/core/src/main/java/io/xpipe/core/process/ShellDialects.java @@ -58,10 +58,15 @@ public class ShellDialects { @Override public void init(ModuleLayer layer) { - ServiceLoader.load(layer, ShellDialect.class).stream().forEach(moduleLayerLoaderProvider -> { + var services = layer != null ? ServiceLoader.load(layer, ShellDialect.class) : ServiceLoader.load(ShellDialect.class); + services.stream().forEach(moduleLayerLoaderProvider -> { ALL.add(moduleLayerLoaderProvider.get()); }); + if (ALL.isEmpty()) { + return; + } + CMD = byId("cmd"); POWERSHELL = byId("powershell"); POWERSHELL_CORE = byId("pwsh"); diff --git a/core/src/main/java/io/xpipe/core/store/DataStoreId.java b/core/src/main/java/io/xpipe/core/store/DataStoreId.java index 6a60bd94..4c24f0b6 100644 --- a/core/src/main/java/io/xpipe/core/store/DataStoreId.java +++ b/core/src/main/java/io/xpipe/core/store/DataStoreId.java @@ -1,6 +1,5 @@ package io.xpipe.core.store; -import com.fasterxml.jackson.annotation.JsonCreator; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -28,7 +27,6 @@ public class DataStoreId { private final List names; - @JsonCreator public DataStoreId(List names) { this.names = names; } diff --git a/core/src/main/java/io/xpipe/core/store/EnabledStoreState.java b/core/src/main/java/io/xpipe/core/store/EnabledStoreState.java new file mode 100644 index 00000000..e7e08f72 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/EnabledStoreState.java @@ -0,0 +1,24 @@ +package io.xpipe.core.store; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.experimental.FieldDefaults; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) +@Getter +@EqualsAndHashCode(callSuper=true) +@SuperBuilder(toBuilder = true) +@Jacksonized +public class EnabledStoreState extends DataStoreState { + + boolean enabled; + + @Override + public DataStoreState mergeCopy(DataStoreState newer) { + var n = (EnabledStoreState) newer; + return EnabledStoreState.builder().enabled(n.enabled).build(); + } +} diff --git a/core/src/main/java/io/xpipe/core/store/FilePath.java b/core/src/main/java/io/xpipe/core/store/FilePath.java index 74b35584..7e0b0231 100644 --- a/core/src/main/java/io/xpipe/core/store/FilePath.java +++ b/core/src/main/java/io/xpipe/core/store/FilePath.java @@ -42,7 +42,6 @@ public final class FilePath { return this; } - var backslash = value.contains("\\"); var p = Pattern.compile("[^/\\\\]+"); var m = p.matcher(value); var replaced = m.replaceAll(matchResult -> osType.makeFileSystemCompatible(matchResult.group())); diff --git a/core/src/main/java/io/xpipe/core/store/LocalStore.java b/core/src/main/java/io/xpipe/core/store/LocalStore.java index f9656abb..b76fbc1f 100644 --- a/core/src/main/java/io/xpipe/core/store/LocalStore.java +++ b/core/src/main/java/io/xpipe/core/store/LocalStore.java @@ -8,7 +8,7 @@ import io.xpipe.core.util.JacksonizedValue; import com.fasterxml.jackson.annotation.JsonTypeName; @JsonTypeName("local") -public class LocalStore extends JacksonizedValue implements ShellStore, StatefulDataStore { +public class LocalStore extends JacksonizedValue implements NetworkTunnelStore, ShellStore, StatefulDataStore { @Override public Class getStateClass() { @@ -23,4 +23,9 @@ public class LocalStore extends JacksonizedValue implements ShellStore, Stateful pc.withShellStateFail(this); return pc; } + + @Override + public DataStore getNetworkParent() { + return null; + } } diff --git a/core/src/main/java/io/xpipe/core/store/NetworkTunnelSession.java b/core/src/main/java/io/xpipe/core/store/NetworkTunnelSession.java new file mode 100644 index 00000000..d0bdd46b --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/NetworkTunnelSession.java @@ -0,0 +1,16 @@ +package io.xpipe.core.store; + +import io.xpipe.core.process.ShellControl; + +public abstract class NetworkTunnelSession extends Session { + + protected NetworkTunnelSession(SessionListener listener) { + super(listener); + } + + public abstract int getLocalPort(); + + public abstract int getRemotePort(); + + public abstract ShellControl getShellControl(); +} diff --git a/core/src/main/java/io/xpipe/core/store/NetworkTunnelStore.java b/core/src/main/java/io/xpipe/core/store/NetworkTunnelStore.java new file mode 100644 index 00000000..92277c15 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/NetworkTunnelStore.java @@ -0,0 +1,149 @@ +package io.xpipe.core.store; + +import io.xpipe.core.process.ShellControl; + +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +public interface NetworkTunnelStore extends DataStore { + + static AtomicInteger portCounter = new AtomicInteger(); + + public static int randomPort() { + var p = 40000 + portCounter.get(); + portCounter.set(portCounter.get() + 1 % 1000); + return p; + } + + interface TunnelFunction { + + NetworkTunnelSession create(int localPort, int remotePort); + } + + DataStore getNetworkParent(); + + default boolean requiresTunnel() { + NetworkTunnelStore current = this; + while (true) { + var func = current.tunnelSession(); + if (func != null) { + return true; + } + + if (current.getNetworkParent() == null) { + return false; + } + + if (current.getNetworkParent() instanceof NetworkTunnelStore t) { + current = t; + } else { + return false; + } + } + } + + default boolean isLocallyTunneable() { + NetworkTunnelStore current = this; + while (true) { + if (current.getNetworkParent() == null) { + return true; + } + + if (current.getNetworkParent() instanceof NetworkTunnelStore t) { + current = t; + } else { + return false; + } + } + } + + default NetworkTunnelSession sessionChain(int local, int remotePort) throws Exception { + if (!isLocallyTunneable()) { + throw new IllegalStateException(); + } + + var running = new AtomicBoolean(); + var runningCounter = new AtomicInteger(); + var counter = new AtomicInteger(); + var sessions = new ArrayList(); + NetworkTunnelStore current = this; + do { + var func = current.tunnelSession(); + if (func == null) { + continue; + } + + var currentLocalPort = isLast(current) ? local : randomPort(); + var currentRemotePort = sessions.isEmpty() ? remotePort : sessions.getLast().getLocalPort(); + var t = func.create(currentLocalPort, currentRemotePort); + t.addListener(r -> { + if (r) { + runningCounter.incrementAndGet(); + } else { + runningCounter.decrementAndGet(); + } + running.set(runningCounter.get() == counter.get()); + }); + t.start(); + sessions.add(t); + counter.incrementAndGet(); + } while ((current = (NetworkTunnelStore) current.getNetworkParent()) != null); + + if (sessions.size() == 1) { + return sessions.getFirst(); + } + + if (sessions.isEmpty()) { + return new NetworkTunnelSession(null) { + + @Override + public boolean isRunning() { + return false; + } + + @Override + public void start() throws Exception { + + } + + @Override + public void stop() throws Exception { + + } + + @Override + public int getLocalPort() { + return remotePort; + } + + @Override + public int getRemotePort() { + return remotePort; + } + + @Override + public ShellControl getShellControl() { + return null; + } + }; + } + + return new SessionChain(running1 -> {}, sessions); + } + + default boolean isLast(NetworkTunnelStore tunnelStore) { + NetworkTunnelStore current = tunnelStore; + while ((current = (NetworkTunnelStore) current.getNetworkParent()) != null) { + var func = current.tunnelSession(); + if (func != null) { + return false; + } + } + return true; + } + + default TunnelFunction tunnelSession() { + return null; + } +} diff --git a/core/src/main/java/io/xpipe/core/store/ServiceStore.java b/core/src/main/java/io/xpipe/core/store/ServiceStore.java new file mode 100644 index 00000000..2763b5b3 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/ServiceStore.java @@ -0,0 +1,23 @@ +package io.xpipe.core.store; + +import java.util.OptionalInt; + +public interface ServiceStore extends SingletonSessionStore { + + NetworkTunnelStore getParent(); + + int getPort(); + + OptionalInt getTargetPort(); + + @Override + default SessionChain newSession() throws Exception { + var s = getParent().tunnelSession(); + return null; + } + + @Override + default Class getSessionClass() { + return null; + } +} diff --git a/core/src/main/java/io/xpipe/core/store/Session.java b/core/src/main/java/io/xpipe/core/store/Session.java index 5607fade..8e02e38c 100644 --- a/core/src/main/java/io/xpipe/core/store/Session.java +++ b/core/src/main/java/io/xpipe/core/store/Session.java @@ -1,10 +1,29 @@ package io.xpipe.core.store; -public abstract class Session { +public abstract class Session implements AutoCloseable { + + protected SessionListener listener; + + protected Session(SessionListener listener) { + this.listener = listener; + } + + public void addListener(SessionListener n) { + var current = this.listener; + this.listener = running -> { + current.onStateChange(running); + n.onStateChange(running); + }; + } public abstract boolean isRunning(); public abstract void start() throws Exception; public abstract void stop() throws Exception; + + @Override + public void close() throws Exception { + stop(); + } } diff --git a/core/src/main/java/io/xpipe/core/store/SessionChain.java b/core/src/main/java/io/xpipe/core/store/SessionChain.java new file mode 100644 index 00000000..98734580 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/SessionChain.java @@ -0,0 +1,51 @@ +package io.xpipe.core.store; + +import io.xpipe.core.process.ShellControl; + +import java.util.List; + +public class SessionChain extends NetworkTunnelSession { + + private final List sessions; + private int runningCounter; + + public SessionChain(SessionListener listener, List sessions) { + super(listener); + this.sessions = sessions; + sessions.forEach(session -> session.addListener(running -> { + runningCounter += running ? 1 : -1; + })); + } + + public ShellControl getShellControl() { + return sessions.getLast().getShellControl(); + } + + public int getLocalPort() { + return sessions.getFirst().getLocalPort(); + } + + @Override + public int getRemotePort() { + return sessions.getLast().getRemotePort(); + } + + @Override + public boolean isRunning() { + return sessions.stream().allMatch(session -> session.isRunning()); + } + + @Override + public void start() throws Exception { + for (Session session : sessions) { + session.start(); + } + } + + @Override + public void stop() throws Exception { + for (Session session : sessions) { + session.stop(); + } + } +} diff --git a/core/src/main/java/io/xpipe/core/store/SessionListener.java b/core/src/main/java/io/xpipe/core/store/SessionListener.java new file mode 100644 index 00000000..45e241e1 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/SessionListener.java @@ -0,0 +1,6 @@ +package io.xpipe.core.store; + +public interface SessionListener { + + void onStateChange(boolean running); +} diff --git a/core/src/main/java/io/xpipe/core/store/SingletonSessionStore.java b/core/src/main/java/io/xpipe/core/store/SingletonSessionStore.java index 3e6a9d06..a2ac66b4 100644 --- a/core/src/main/java/io/xpipe/core/store/SingletonSessionStore.java +++ b/core/src/main/java/io/xpipe/core/store/SingletonSessionStore.java @@ -1,6 +1,7 @@ package io.xpipe.core.store; -public interface SingletonSessionStore extends ExpandedLifecycleStore, InternalCacheDataStore { +public interface SingletonSessionStore + extends ExpandedLifecycleStore, InternalCacheDataStore, SessionListener { @Override default void finalizeValidate() throws Exception { @@ -19,9 +20,10 @@ public interface SingletonSessionStore extends ExpandedLifecy return getCache("sessionEnabled", Boolean.class, false); } - default void onSessionUpdate(boolean active) { - setSessionEnabled(active); - setCache("sessionRunning", active); + @Override + default void onStateChange(boolean running) { + setSessionEnabled(running); + setCache("sessionRunning", running); } T newSession() throws Exception; @@ -50,9 +52,9 @@ public interface SingletonSessionStore extends ExpandedLifecy s = newSession(); s.start(); setCache("session", s); - onSessionUpdate(true); + onStateChange(true); } catch (Exception ex) { - onSessionUpdate(false); + onStateChange(false); throw ex; } } @@ -65,7 +67,7 @@ public interface SingletonSessionStore extends ExpandedLifecy if (ex != null) { ex.stop(); setCache("session", null); - onSessionUpdate(false); + onStateChange(false); } } } diff --git a/core/src/main/java/io/xpipe/core/store/StorePath.java b/core/src/main/java/io/xpipe/core/store/StorePath.java new file mode 100644 index 00000000..4d3b47ba --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/StorePath.java @@ -0,0 +1,84 @@ +package io.xpipe.core.store; + +import com.fasterxml.jackson.annotation.JsonCreator; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Represents a reference to an XPipe storage location. + *

+ * To allow for a simple usage, the names are trimmed and + * converted to lower case names when creating them. + * The names are separated by a slash and are therefore not allowed to contain slashes themselves. + * + * @see #fromString(String) + */ +@EqualsAndHashCode +@Getter +public class StorePath { + + public static final char SEPARATOR = '/'; + + private final List names; + + @JsonCreator + public StorePath(List names) { + this.names = names; + } + + /** + * Creates a new store path. + * + * @throws IllegalArgumentException if any name is not valid + */ + public static StorePath create(String... names) { + if (names == null) { + throw new IllegalArgumentException("Names are null"); + } + + if (Arrays.stream(names).anyMatch(s -> s == null)) { + throw new IllegalArgumentException("Name is null"); + } + + if (Arrays.stream(names).anyMatch(s -> s.contains("" + SEPARATOR))) { + throw new IllegalArgumentException("Separator character " + SEPARATOR + " is not allowed in the names"); + } + + if (Arrays.stream(names).anyMatch(s -> s.trim().length() == 0)) { + throw new IllegalArgumentException("Trimmed entry name is empty"); + } + + return new StorePath(Arrays.stream(names).toList()); + } + + /** + * Creates a new store path from a string representation. + * + * @param s the string representation, must be not null and fulfill certain requirements + * @throws IllegalArgumentException if the string is not valid + */ + public static StorePath fromString(String s) { + if (s == null) { + throw new IllegalArgumentException("String is null"); + } + + var split = s.split(String.valueOf(SEPARATOR), -1); + + var names = + Arrays.stream(split).map(String::trim).map(String::toLowerCase).toList(); + if (names.stream().anyMatch(s1 -> s1.isEmpty())) { + throw new IllegalArgumentException("Name must not be empty"); + } + + return new StorePath(names); + } + + @Override + public String toString() { + return names.stream().map(String::toLowerCase).collect(Collectors.joining("" + SEPARATOR)); + } +} 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 8699ffbd..0b3dd93c 100644 --- a/core/src/main/java/io/xpipe/core/util/JacksonMapper.java +++ b/core/src/main/java/io/xpipe/core/util/JacksonMapper.java @@ -49,14 +49,14 @@ public class JacksonMapper { mapper.accept(INSTANCE); } - public static synchronized void initClassBased() { - initModularized(null); - } + public static class Loader implements ModuleLayerLoader { - public static synchronized void initModularized(ModuleLayer layer) { - List MODULES = findModules(layer); - INSTANCE.registerModules(MODULES); - init = true; + @Override + public void init(ModuleLayer layer) { + List MODULES = findModules(layer); + INSTANCE.registerModules(MODULES); + init = true; + } } private static List findModules(ModuleLayer layer) { diff --git a/core/src/main/java/io/xpipe/core/util/ModuleLayerLoader.java b/core/src/main/java/io/xpipe/core/util/ModuleLayerLoader.java index 216c97ec..e78dcab2 100644 --- a/core/src/main/java/io/xpipe/core/util/ModuleLayerLoader.java +++ b/core/src/main/java/io/xpipe/core/util/ModuleLayerLoader.java @@ -6,7 +6,8 @@ import java.util.function.Consumer; public interface ModuleLayerLoader { static void loadAll(ModuleLayer layer, Consumer errorHandler) { - ServiceLoader.load(layer, ModuleLayerLoader.class).stream().forEach(moduleLayerLoaderProvider -> { + var loaded = layer != null ? ServiceLoader.load(layer, ModuleLayerLoader.class) : ServiceLoader.load(ModuleLayerLoader.class); + loaded.stream().forEach(moduleLayerLoaderProvider -> { var instance = moduleLayerLoaderProvider.get(); try { instance.init(layer); 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 4f89bb1a..074e85a8 100644 --- a/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java +++ b/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java @@ -22,18 +22,18 @@ public class XPipeInstallation { .orElse(false); public static int getDefaultBeaconPort() { - var offset = isStaging() ? 1 : 0; - if (OsType.getLocal().equals(OsType.WINDOWS)) { - return 21721 + offset; - } else { - return 21721 + 2 + offset; - } + var offset = isStaging() ? 2 : 0; + return 21721 + offset; } private static String getPkgId() { return isStaging() ? "io.xpipe.xpipe-ptb" : "io.xpipe.xpipe"; } + public static Path getLocalBeaconAuthFile() { + return Path.of(System.getProperty("java.io.tmpdir"), "xpipe_auth"); + } + public static String createExternalAsyncLaunchCommand( String installationBase, XPipeDaemonMode mode, String arguments, boolean restart) { var suffix = (arguments != null ? " " + arguments : ""); diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index 287db7ad..0bb231b2 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -2,6 +2,7 @@ import io.xpipe.core.process.ProcessControlProvider; import io.xpipe.core.process.ShellDialect; import io.xpipe.core.process.ShellDialects; import io.xpipe.core.util.CoreJacksonModule; +import io.xpipe.core.util.JacksonMapper; import io.xpipe.core.util.ModuleLayerLoader; open module io.xpipe.core { @@ -11,9 +12,9 @@ open module io.xpipe.core { exports io.xpipe.core.process; requires com.fasterxml.jackson.datatype.jsr310; - requires com.fasterxml.jackson.module.paramnames; - requires static com.fasterxml.jackson.core; - requires static com.fasterxml.jackson.databind; + requires com.fasterxml.jackson.core; + requires com.fasterxml.jackson.annotation; + requires com.fasterxml.jackson.databind; requires java.net.http; requires static lombok; @@ -24,7 +25,7 @@ open module io.xpipe.core { uses ModuleLayerLoader; uses ShellDialect; - provides ModuleLayerLoader with + provides ModuleLayerLoader with JacksonMapper.Loader, ShellDialects.Loader; provides com.fasterxml.jackson.databind.Module with CoreJacksonModule; diff --git a/core/src/main/resources/META-INF/services/io.xpipe.core.util.ModuleLayerLoader b/core/src/main/resources/META-INF/services/io.xpipe.core.util.ModuleLayerLoader new file mode 100644 index 00000000..c61f23fd --- /dev/null +++ b/core/src/main/resources/META-INF/services/io.xpipe.core.util.ModuleLayerLoader @@ -0,0 +1,2 @@ +io.xpipe.core.util.JacksonMapper$Loader +io.xpipe.core.process.ShellDialects$Loader \ No newline at end of file diff --git a/dist/build.gradle b/dist/build.gradle index 25e480bd..7f5f09b9 100644 --- a/dist/build.gradle +++ b/dist/build.gradle @@ -2,8 +2,8 @@ plugins { id 'org.beryx.jlink' version '3.0.1' id "org.asciidoctor.jvm.convert" version "4.0.2" - id 'org.jreleaser' version '1.11.0' - id("com.netflix.nebula.ospackage") version "11.8.1" + id 'org.jreleaser' version '1.12.0' + id("com.netflix.nebula.ospackage") version "11.9.1" id 'org.gradle.crypto.checksum' version '1.4.0' id 'signing' } diff --git a/dist/changelogs/10.0.md b/dist/changelogs/10.0.md new file mode 100644 index 00000000..711fda1b --- /dev/null +++ b/dist/changelogs/10.0.md @@ -0,0 +1,72 @@ +## 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 spec 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. + +There will be more functionality added to the API in the future, for now this initial implementation is open for feedback. + +## 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. + +## 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. + +## 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. + +Note that old docker container connections will be removed as they are incompatible with the new version. Any other subconnections like shell environments in docker containers will persist, although they might get invalidated and will show up on the bottom of the connection list. + +There's now support for Windows docker containers running on HyperV. + +## 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 + +- Several more actions have been added for podman containers +- Support VMs for tunneling +- Searching for connections has been improved to show children as well +- The welcome screen will now also contain the option to straight up jump to the synchronization settings +- Add support for foot terminal +- Fix elementary terminal not launching correctly +- 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/9.4.1.md b/dist/changelogs/9.4.1.md new file mode 100644 index 00000000..de6f3a75 --- /dev/null +++ b/dist/changelogs/9.4.1.md @@ -0,0 +1,59 @@ +## Coherent desktops + +XPipe now comes with support for remote desktop connections. VNC connections are fully handled over SSH and can therefore be established on top of any existing SSH connection you have in XPipe. RDP support is realized similar to the terminal support, i.e. by launching your preferred RDP client with the connection information. X11-forwarding for SSH is also now supported. + +With support for remote graphical desktop connection methods as well now in XPipe 9, the big picture idea is to implement the concept of coherent desktops. Essentially, you can launch predefined desktop applications, terminals, and scripts on any remote desktop connection, regardless of the underlying connection implementation. In combination with the improved SSH tunnel and background session support, you can launch graphical remote applications with one click in the same unified way for VNC over SSH connections, RDP connections, and X11-forwarded SSH connections. + +The general implementation and concept will be refined over the next updates. + +## SSH connection improvements + +- Tunneled and X11-forwarded custom SSH connections are now properly detected and can be toggled on and off to run in the background as normal tunnels. This applies to normal connections and also SSH configs + +- The connection establishment has been reworked to reduce the amount of double prompts, e.g. for smartcards or 2FA, where user input is required twice. + +- The custom SSH connections now properly apply all configuration options of your user configuration file. They also now correctly apply multiple options for the same key correctly. + +- Any value specified for the `RemoteCommand` config option will now be properly applied when launching a terminal. This allows you to still use your preexisting init command setup, e.g. with tmux. + +- There is now support defining multiple host entries in place in a custom SSH connection. This is useful for cases where you want to use ProxyJump hosts in place without having to define them elsewhere. + +- A host key acceptance notification is now displayed properly in case your system doesn't automatically accept new host keys + +## SSH for unknown shells (Professional feature) + +There's now an option to not let XPipe interact with the system. In case a system that does not run a known command shell, e.g. a router, link, or some IOT device, XPipe was previously unable to detect the shell type and errored out after some time. This option fixes this problem. This feature is available in the professional edition preview for two weeks. + +## SSH X11 Forwarding on Windows via WSL + +You can now enable X11 forwarding for an SSH connection. + +XPipe allows you to use the WSL2 X11 capabilities on Windows for your SSH connection. The only thing you need for this is a [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) distribution installed on your local system. XPipe it will automatically choose a compatible installed distribution if possible, but you can also use another one in the settings menu. + +This means that you don't need to install a separate X11 server on Windows. However, if you are using one anyway, XPipe will detect that and use the currently running X11 server. + +## Translations + +XPipe 9 now comes with translations for the user interface. These were initially generated with DeepL and can be easily improved and corrected by anyone on GitHub. You can check them out in action and if there is any translation you don't like, submit a quick pull request to fix it. For instructions on how to do this, see https://github.com/xpipe-io/xpipe/tree/master/lang. + +## Terminal improvements + +The terminal integrations have been reworked across the board. To better show which terminals are well supported and which aren't, there is now a status indicator for every available terminal. This will show you how good the XPipe integration with each one is and which terminals are recommended to be used with XPipe. + +The kitty terminal is now fully supported with tabs on both Linux and macOS. The Warp terminal integration now correctly enables all Warp features on remote shells. On macOS, other third-party prompts also now work properly in the launched terminals. + +## Password manager improvements + +The password manager handling has been improved and some potential sources of errors and confusion have been eliminated. There are also now a few command templates available for established password managers to quickly get started. + +## Improved keyboard control + +It is a goal to be able to use XPipe only with a keyboard either for productivity or for accessibility reasons. XPipe 9 introduces improved keyboard support with new shortcuts and improved focus control for navigating with the arrow keys, tab, space, and enter. + +## Improved logo + +The application logo has been improved with of regards to contrast and visibility, which often was a problem on dark backgrounds. It should now stand out on any background color. + +## Other changes + +There have been countless small bug fixes across the board. They are not listed individually here, but hopefully you will notice some of them. diff --git a/dist/changelogs/9.4.1_incremental.md b/dist/changelogs/9.4.1_incremental.md new file mode 100644 index 00000000..2a594223 --- /dev/null +++ b/dist/changelogs/9.4.1_incremental.md @@ -0,0 +1,3 @@ +- Make passwords for SSH config connections with set identity file automatically default to none, saving some manual configuration +- Fix terminal installation detection being broken on macOS, always defaulting to kitty.app +- Some small file IO performance improvements diff --git a/dist/changelogs/9.4_incremental.md b/dist/changelogs/9.4_incremental.md index 09f53bde..cc1a9e9d 100644 --- a/dist/changelogs/9.4_incremental.md +++ b/dist/changelogs/9.4_incremental.md @@ -8,6 +8,8 @@ The file transfer mechanism when editing files had some flaws, which under rare The entire transfer implementation has been rewritten to iron out these issues and increase reliability. Other file browser actions have also been made more reliable. +There seems to be another separate issue with a PowerShell bug when connecting to a Windows system, causing file uploads to be slow. For now, xpipe can fall back to pwsh if it is installed to work around this issue. + ## Git vault improvements The conflict resolution has been improved @@ -15,11 +17,27 @@ The conflict resolution has been improved - In case of a merge conflict, overwriting local changes will now preserve all connections that are not added to the git vault, including local connections - You now have the option to force push changes when a conflict occurs while XPipe is saving while running, not requiring a restart anymore +## Terminal improvements + +The terminal integration got reworked for some terminals: +- iTerm can now launch tabs instead of individual windows. There were also a few issues fixed that prevented it from launching sometimes +- WezTerm now supports tabs on Linux and macOS. The Windows installation detection has been improved to detect all installed versions +- Terminal.app will now launch faster + ## Other - You can now add simple RDP connections without a file - Fix VMware Player/Workstation and MSYS2 not being detected on Windows. Now simply searching for connections should add them automatically if they are installed - The file browser sidebar now only contains connections that can be opened in it, reducing the amount of connection shown +- Clarify error message for RealVNC servers, highlighting that RealVNC uses a proprietary protocol spec that can't be supported by third-party VNC clients like xpipe +- Fix Linux builds containing unnecessary debug symbols +- Fix AUR package also installing a debug package +- Fix application restart not working properly on macOS +- Fix possibility of selecting own children connections as hosts, causing a stack overflow. Please don't try to create cycles in your connection graphs +- Fix vault secrets not correctly updating unless restarted when changing vault passphrase +- Fix connection launcher desktop shortcuts and URLs not properly executing if xpipe is not running +- Fix move to ... menu sometimes not ordering categories correctly - Fix SSH command failing on macOS with homebrew openssh package installed -- Fix SSH connections not opening the correct shell environment on Windows when username contained spaces due to an OpenSSH bug +- Fix SSH connections not opening the correct shell environment on Windows systems when username contained spaces due to an OpenSSH bug - Fix newly added connections not having the correct order +- Fix error messages of external editor programs not being shown when they failed to start diff --git a/dist/jpackage.gradle b/dist/jpackage.gradle index b4dd6a47..62ae673d 100644 --- a/dist/jpackage.gradle +++ b/dist/jpackage.gradle @@ -58,7 +58,7 @@ jlink { ] if (org.gradle.internal.os.OperatingSystem.current().isLinux()) { - options += ['--strip-native-debug-symbols'] + options.addAll('--strip-native-debug-symbols', 'exclude-debuginfo-files') } if (useBundledJavaFx) { diff --git a/dist/licenses/prettytime.license b/dist/licenses/prettytime.license deleted file mode 100644 index f433b1a5..00000000 --- a/dist/licenses/prettytime.license +++ /dev/null @@ -1,177 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/dist/licenses/prettytime.properties b/dist/licenses/prettytime.properties deleted file mode 100644 index 0c67b412..00000000 --- a/dist/licenses/prettytime.properties +++ /dev/null @@ -1,4 +0,0 @@ -name=Prettytime -version=5.0.7.Final -license=Apache License 2.0 -link=https://github.com/ocpsoft/prettytime \ No newline at end of file diff --git a/dist/logo/hicolor/1024x1024/apps/xpipe.png b/dist/logo/hicolor/1024x1024/apps/xpipe.png new file mode 100644 index 00000000..643d6acc Binary files /dev/null and b/dist/logo/hicolor/1024x1024/apps/xpipe.png differ diff --git a/dist/logo/hicolor/128x128/apps/xpipe.png b/dist/logo/hicolor/128x128/apps/xpipe.png new file mode 100644 index 00000000..b7811622 Binary files /dev/null and b/dist/logo/hicolor/128x128/apps/xpipe.png differ diff --git a/dist/logo/hicolor/16x16/apps/xpipe.png b/dist/logo/hicolor/16x16/apps/xpipe.png new file mode 100644 index 00000000..d05eb522 Binary files /dev/null and b/dist/logo/hicolor/16x16/apps/xpipe.png differ diff --git a/dist/logo/hicolor/256x256/apps/xpipe.png b/dist/logo/hicolor/256x256/apps/xpipe.png new file mode 100644 index 00000000..c935a751 Binary files /dev/null and b/dist/logo/hicolor/256x256/apps/xpipe.png differ diff --git a/dist/logo/hicolor/32x32/apps/xpipe.png b/dist/logo/hicolor/32x32/apps/xpipe.png new file mode 100644 index 00000000..2040668c Binary files /dev/null and b/dist/logo/hicolor/32x32/apps/xpipe.png differ diff --git a/dist/logo/hicolor/48x48/apps/xpipe.png b/dist/logo/hicolor/48x48/apps/xpipe.png new file mode 100644 index 00000000..b9ebbc06 Binary files /dev/null and b/dist/logo/hicolor/48x48/apps/xpipe.png differ diff --git a/dist/logo/hicolor/512x512/apps/xpipe.png b/dist/logo/hicolor/512x512/apps/xpipe.png new file mode 100644 index 00000000..7afdbf9b Binary files /dev/null and b/dist/logo/hicolor/512x512/apps/xpipe.png differ diff --git a/dist/logo/hicolor/64x64/apps/xpipe.png b/dist/logo/hicolor/64x64/apps/xpipe.png new file mode 100644 index 00000000..ce5fa518 Binary files /dev/null and b/dist/logo/hicolor/64x64/apps/xpipe.png differ diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/BrowseStoreAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/BrowseStoreAction.java index 1995f863..1d82465d 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/BrowseStoreAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/BrowseStoreAction.java @@ -17,8 +17,8 @@ import lombok.Value; public class BrowseStoreAction implements ActionProvider { @Override - public DataStoreCallSite getDataStoreCallSite() { - return new DataStoreCallSite() { + public LeafDataStoreCallSite getLeafDataStoreCallSite() { + return new LeafDataStoreCallSite() { @Override public boolean isApplicable(DataStoreEntryRef o) { @@ -63,11 +63,6 @@ public class BrowseStoreAction implements ActionProvider { DataStoreEntry entry; - @Override - public boolean requiresJavaFXPlatform() { - return true; - } - @Override public void execute() { BrowserSessionModel.DEFAULT.openFileSystemAsync(entry.ref(), null, new SimpleBooleanProperty()); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/CloneStoreAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/CloneStoreAction.java index 7a30122d..4795885c 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/CloneStoreAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/CloneStoreAction.java @@ -14,8 +14,8 @@ import lombok.Value; public class CloneStoreAction implements ActionProvider { @Override - public DataStoreCallSite getDataStoreCallSite() { - return new DataStoreCallSite<>() { + public LeafDataStoreCallSite getLeafDataStoreCallSite() { + return new LeafDataStoreCallSite<>() { @Override public boolean isSystemAction() { @@ -54,11 +54,6 @@ public class CloneStoreAction implements ActionProvider { DataStoreEntry store; - @Override - public boolean requiresJavaFXPlatform() { - return false; - } - @Override public void execute() { DataStorage.get() diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/DeleteStoreChildrenAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/DeleteChildrenStoreAction.java similarity index 86% rename from ext/base/src/main/java/io/xpipe/ext/base/action/DeleteStoreChildrenAction.java rename to ext/base/src/main/java/io/xpipe/ext/base/action/DeleteChildrenStoreAction.java index 7d46ea3c..ed69e316 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/DeleteStoreChildrenAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/DeleteChildrenStoreAction.java @@ -12,11 +12,11 @@ import javafx.beans.value.ObservableValue; import lombok.Value; -public class DeleteStoreChildrenAction implements ActionProvider { +public class DeleteChildrenStoreAction implements ActionProvider { @Override - public DataStoreCallSite getDataStoreCallSite() { - return new DataStoreCallSite<>() { + public LeafDataStoreCallSite getLeafDataStoreCallSite() { + return new LeafDataStoreCallSite<>() { @Override public boolean isSystemAction() { @@ -56,11 +56,6 @@ public class DeleteStoreChildrenAction implements ActionProvider { DataStoreEntry store; - @Override - public boolean requiresJavaFXPlatform() { - return false; - } - @Override public void execute() { DataStorage.get().deleteChildren(store); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/EditStoreAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/EditStoreAction.java index 07355511..f01d8e95 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 @@ -14,8 +14,8 @@ import lombok.Value; public class EditStoreAction implements ActionProvider { @Override - public DataStoreCallSite getDataStoreCallSite() { - return new DataStoreCallSite<>() { + public LeafDataStoreCallSite getLeafDataStoreCallSite() { + return new LeafDataStoreCallSite<>() { @Override public boolean isSystemAction() { @@ -49,8 +49,8 @@ public class EditStoreAction implements ActionProvider { } @Override - public ActiveType activeType() { - return ActiveType.ALWAYS_ENABLE; + public boolean requiresValidStore() { + return false; } }; } @@ -85,11 +85,6 @@ public class EditStoreAction implements ActionProvider { DataStoreEntry store; - @Override - public boolean requiresJavaFXPlatform() { - return true; - } - @Override public void execute() { StoreCreationComp.showEdit(store); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchStoreAction.java similarity index 92% rename from ext/base/src/main/java/io/xpipe/ext/base/action/LaunchAction.java rename to ext/base/src/main/java/io/xpipe/ext/base/action/LaunchStoreAction.java index 6f121cbd..450eb5a9 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/LaunchStoreAction.java @@ -15,7 +15,7 @@ import javafx.beans.value.ObservableValue; import lombok.Value; -public class LaunchAction implements ActionProvider { +public class LaunchStoreAction implements ActionProvider { @Override public String getId() { @@ -23,8 +23,8 @@ public class LaunchAction implements ActionProvider { } @Override - public DataStoreCallSite getDataStoreCallSite() { - return new DataStoreCallSite<>() { + public LeafDataStoreCallSite getLeafDataStoreCallSite() { + return new LeafDataStoreCallSite<>() { @Override public boolean canLinkTo() { @@ -88,11 +88,6 @@ public class LaunchAction implements ActionProvider { DataStoreEntry entry; - @Override - public boolean requiresJavaFXPlatform() { - return false; - } - @Override public void execute() throws Exception { var storeName = DataStorage.get().getStoreDisplayName(entry); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/RefreshStoreChildrenAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/RefreshChildrenStoreAction.java similarity index 87% rename from ext/base/src/main/java/io/xpipe/ext/base/action/RefreshStoreChildrenAction.java rename to ext/base/src/main/java/io/xpipe/ext/base/action/RefreshChildrenStoreAction.java index 0c6e0748..cc41e806 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/RefreshStoreChildrenAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/RefreshChildrenStoreAction.java @@ -6,16 +6,14 @@ import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.util.FixedHierarchyStore; - import javafx.beans.value.ObservableValue; - import lombok.Value; -public class RefreshStoreChildrenAction implements ActionProvider { +public class RefreshChildrenStoreAction implements ActionProvider { @Override - public ActionProvider.DataStoreCallSite getDataStoreCallSite() { - return new ActionProvider.DataStoreCallSite() { + public LeafDataStoreCallSite getLeafDataStoreCallSite() { + return new LeafDataStoreCallSite() { @Override public ActionProvider.Action createAction(DataStoreEntryRef store) { @@ -70,11 +68,6 @@ public class RefreshStoreChildrenAction implements ActionProvider { DataStoreEntry store; - @Override - public boolean requiresJavaFXPlatform() { - return false; - } - @Override public void execute() { DataStorage.get().refreshChildren(store); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/SampleAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/SampleStoreAction.java similarity index 94% rename from ext/base/src/main/java/io/xpipe/ext/base/action/SampleAction.java rename to ext/base/src/main/java/io/xpipe/ext/base/action/SampleStoreAction.java index 5fd35308..27b2db2d 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/SampleAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/SampleStoreAction.java @@ -18,13 +18,13 @@ import lombok.Value; import java.io.BufferedReader; import java.io.InputStreamReader; -public class SampleAction implements ActionProvider { +public class SampleStoreAction implements ActionProvider { @Override - public DataStoreCallSite getDataStoreCallSite() { + public LeafDataStoreCallSite getLeafDataStoreCallSite() { // Call sites represent different ways of invoking the action. // In this case, this represents a button that is shown for all stored shell connections. - return new DataStoreCallSite() { + return new LeafDataStoreCallSite() { @Override public Action createAction(DataStoreEntryRef store) { @@ -63,12 +63,6 @@ public class SampleAction implements ActionProvider { DataStoreEntry entry; - @Override - public boolean requiresJavaFXPlatform() { - // Do we require the JavaFX platform to be running? - return false; - } - @Override public void execute() throws Exception { var docker = new LocalStore(); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/ScanAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/ScanStoreAction.java similarity index 88% rename from ext/base/src/main/java/io/xpipe/ext/base/action/ScanAction.java rename to ext/base/src/main/java/io/xpipe/ext/base/action/ScanStoreAction.java index b2a2bbfa..7064ff55 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/ScanAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/ScanStoreAction.java @@ -12,11 +12,11 @@ import javafx.beans.value.ObservableValue; import lombok.Value; -public class ScanAction implements ActionProvider { +public class ScanStoreAction implements ActionProvider { @Override - public DataStoreCallSite getDataStoreCallSite() { - return new DataStoreCallSite() { + public LeafDataStoreCallSite getLeafDataStoreCallSite() { + return new LeafDataStoreCallSite() { @Override public ActionProvider.Action createAction(DataStoreEntryRef store) { @@ -65,11 +65,6 @@ public class ScanAction implements ActionProvider { DataStoreEntry entry; - @Override - public boolean requiresJavaFXPlatform() { - return true; - } - @Override public void execute() { ScanAlert.showAsync(entry); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/ShareStoreAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/ShareStoreAction.java index 8aba7de9..911c78f5 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/ShareStoreAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/ShareStoreAction.java @@ -16,8 +16,8 @@ import lombok.Value; public class ShareStoreAction implements ActionProvider { @Override - public DataStoreCallSite getDataStoreCallSite() { - return new DataStoreCallSite<>() { + public LeafDataStoreCallSite getLeafDataStoreCallSite() { + return new LeafDataStoreCallSite<>() { @Override public ActionProvider.Action createAction(DataStoreEntryRef store) { @@ -55,11 +55,6 @@ public class ShareStoreAction implements ActionProvider { return "xpipe://addStore/" + InPlaceSecretValue.of(store.toString()).getEncryptedValue(); } - @Override - public boolean requiresJavaFXPlatform() { - return false; - } - @Override public void execute() { var url = create(store.getStore()); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/XPipeUrlAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/XPipeUrlAction.java index 10f3e926..7a7807a0 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/XPipeUrlAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/XPipeUrlAction.java @@ -43,13 +43,13 @@ public class XPipeUrlAction implements ActionProvider { if (!entry.getValidity().isUsable()) { return null; } - return new LaunchAction.Action(entry); + return new LaunchStoreAction.Action(entry); } case "action" -> { var id = args.get(1); ActionProvider provider = ActionProvider.ALL.stream() .filter(actionProvider -> { - return actionProvider.getDataStoreCallSite() != null + return actionProvider.getLeafDataStoreCallSite() != null && id.equals(actionProvider.getId()); }) .findFirst() @@ -83,14 +83,9 @@ public class XPipeUrlAction implements ActionProvider { ActionProvider actionProvider; DataStoreEntry entry; - @Override - public boolean requiresJavaFXPlatform() { - return false; - } - @Override public void execute() throws Exception { - actionProvider.getDataStoreCallSite().createAction(entry.ref()).execute(); + actionProvider.getLeafDataStoreCallSite().createAction(entry.ref()).execute(); } } @@ -99,11 +94,6 @@ public class XPipeUrlAction implements ActionProvider { DataStore store; - @Override - public boolean requiresJavaFXPlatform() { - return true; - } - @Override public void execute() { if (store == null) { diff --git a/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopApplicationStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopApplicationStoreProvider.java index cacc3e60..447bf184 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopApplicationStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopApplicationStoreProvider.java @@ -31,10 +31,6 @@ public class DesktopApplicationStoreProvider implements DataStoreProvider { @Override public ActionProvider.Action launchAction(DataStoreEntry store) { return new ActionProvider.Action() { - @Override - public boolean requiresJavaFXPlatform() { - return false; - } @Override public void execute() throws Exception { diff --git a/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopCommandStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopCommandStoreProvider.java index 0d0eca75..78baedf9 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopCommandStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopCommandStoreProvider.java @@ -32,10 +32,6 @@ public class DesktopCommandStoreProvider implements DataStoreProvider { @Override public ActionProvider.Action launchAction(DataStoreEntry store) { return new ActionProvider.Action() { - @Override - public boolean requiresJavaFXPlatform() { - return false; - } @Override public void execute() throws Exception { diff --git a/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopEnvironmentStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopEnvironmentStoreProvider.java index c46a8eb0..dc9972ba 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopEnvironmentStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopEnvironmentStoreProvider.java @@ -39,10 +39,6 @@ public class DesktopEnvironmentStoreProvider implements DataStoreProvider { @Override public ActionProvider.Action activateAction(DataStoreEntry store) { return new ActionProvider.Action() { - @Override - public boolean requiresJavaFXPlatform() { - return false; - } @Override public void execute() throws Exception { @@ -61,10 +57,6 @@ public class DesktopEnvironmentStoreProvider implements DataStoreProvider { @Override public ActionProvider.Action launchAction(DataStoreEntry store) { return new ActionProvider.Action() { - @Override - public boolean requiresJavaFXPlatform() { - return false; - } @Override public void execute() throws Exception { diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/PredefinedScriptGroup.java b/ext/base/src/main/java/io/xpipe/ext/base/script/PredefinedScriptGroup.java index dfc07e47..84d7c917 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/PredefinedScriptGroup.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/PredefinedScriptGroup.java @@ -8,7 +8,9 @@ import lombok.Setter; @Getter public enum PredefinedScriptGroup { CLINK("Clink", null, false), - STARSHIP("Starship", "Sets up and enables the starship shell prompt", true); + STARSHIP("Starship", "Sets up and enables the starship shell prompt", true), + MANAGEMENT("Management", "Some commonly used management scripts", true), + FILES("Files", "Scripts for files", true); private final String name; private final String description; 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 e74ecffa..8d8e043d 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 @@ -19,11 +19,13 @@ public enum PredefinedScriptStore { .group(PredefinedScriptGroup.CLINK.getEntry()) .minimumDialect(ShellDialects.CMD) .commands(file("clink.bat")) + .initScript(true) .build()), CLINK_INJECT("Clink Inject", () -> SimpleScriptStore.builder() .group(PredefinedScriptGroup.CLINK.getEntry()) .minimumDialect(ShellDialects.CMD) .script(CLINK_SETUP.getEntry()) + .initScript(true) .commands(""" clink inject --quiet """) @@ -32,27 +34,44 @@ public enum PredefinedScriptStore { .group(PredefinedScriptGroup.STARSHIP.getEntry()) .minimumDialect(ShellDialects.BASH) .commands(file("starship_bash.sh")) + .initScript(true) .build()), STARSHIP_ZSH("Starship Zsh", () -> SimpleScriptStore.builder() .group(PredefinedScriptGroup.STARSHIP.getEntry()) .minimumDialect(ShellDialects.ZSH) .commands(file("starship_zsh.sh")) + .initScript(true) .build()), STARSHIP_FISH("Starship Fish", () -> SimpleScriptStore.builder() .group(PredefinedScriptGroup.STARSHIP.getEntry()) .minimumDialect(ShellDialects.FISH) .commands(file("starship_fish.fish")) + .initScript(true) .build()), STARSHIP_CMD("Starship Cmd", () -> SimpleScriptStore.builder() .group(PredefinedScriptGroup.STARSHIP.getEntry()) .minimumDialect(ShellDialects.CMD) .script(CLINK_SETUP.getEntry()) .commands(file(("starship_cmd.bat"))) + .initScript(true) .build()), STARSHIP_POWERSHELL("Starship Powershell", () -> SimpleScriptStore.builder() .group(PredefinedScriptGroup.STARSHIP.getEntry()) .minimumDialect(ShellDialects.POWERSHELL) .commands(file("starship_powershell.ps1")) + .initScript(true) + .build()), + APT_UPDATE("Apt update", () -> SimpleScriptStore.builder() + .group(PredefinedScriptGroup.MANAGEMENT.getEntry()) + .minimumDialect(ShellDialects.SH) + .commands(file(("apt_update.sh"))) + .shellScript(true) + .build()), + REMOVE_CR("CRLF to LF", () -> SimpleScriptStore.builder() + .group(PredefinedScriptGroup.FILES.getEntry()) + .minimumDialect(ShellDialects.SH) + .commands(file(("crlf_to_lf.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 new file mode 100644 index 00000000..0cb12bf4 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/RunScriptAction.java @@ -0,0 +1,95 @@ +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.storage.DataStorage; +import io.xpipe.app.util.ScriptHelper; +import io.xpipe.core.process.ShellControl; +import io.xpipe.core.store.FilePath; +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 { + + @Override + public Node getIcon(OpenFileSystemModel model, List entries) { + return new FontIcon("mdi2c-code-greater-than"); + } + + @Override + public Category getCategory() { + return Category.MUTATION; + } + + @Override + public ObservableValue getName(OpenFileSystemModel model, List entries) { + return AppI18n.observable("runScript"); + } + + @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.assemble(sc) == null) { + continue; + } + + var entry = DataStorage.get().getStoreEntryIfPresent(script, true); + if (entry.isPresent()) { + map.put(entry.get().getName(), script); + } + } + return map; + } + + @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); + sc.executeSimpleCommand(sc.getShellDialect().runScriptCommand(sc, script.toString()) + " " + args); + } + } + + @Override + public ObservableValue getName(OpenFileSystemModel model, List entries) { + return new SimpleStringProperty(e.getKey()); + } + }; + }) + .map(leafAction -> (LeafAction) leafAction) + .toList(); + return actions; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptDataStorageProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptDataStorageProvider.java new file mode 100644 index 00000000..d1fe28e4 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptDataStorageProvider.java @@ -0,0 +1,50 @@ +package io.xpipe.ext.base.script; + +import io.xpipe.app.ext.DataStorageExtensionProvider; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +public class ScriptDataStorageProvider extends DataStorageExtensionProvider { + + @Override + public void storageInit() { + DataStorage.get() + .addStoreEntryIfNotPresent(DataStoreEntry.createNew( + UUID.fromString("a9945ad2-db61-4304-97d7-5dc4330691a7"), + DataStorage.CUSTOM_SCRIPTS_CATEGORY_UUID, + "My scripts", + ScriptGroupStore.builder().build())); + + for (PredefinedScriptGroup value : PredefinedScriptGroup.values()) { + ScriptGroupStore store = ScriptGroupStore.builder() + .description(value.getDescription()) + .build(); + var e = DataStorage.get() + .addStoreEntryIfNotPresent(DataStoreEntry.createNew( + UUID.nameUUIDFromBytes(("a " + value.getName()).getBytes(StandardCharsets.UTF_8)), + DataStorage.PREDEFINED_SCRIPTS_CATEGORY_UUID, + value.getName(), + store)); + e.setStoreInternal(store, false); + e.setExpanded(value.isExpanded()); + value.setEntry(e.ref()); + } + + for (PredefinedScriptStore value : PredefinedScriptStore.values()) { + var previous = DataStorage.get().getStoreEntryIfPresent(value.getUuid()); + var store = value.getScriptStore().get(); + if (previous.isPresent()) { + previous.get().setStoreInternal(store, false); + value.setEntry(previous.get().ref()); + } else { + var e = DataStoreEntry.createNew( + value.getUuid(), DataStorage.PREDEFINED_SCRIPTS_CATEGORY_UUID, value.getName(), store); + DataStorage.get().addStoreEntryIfNotPresent(e); + value.setEntry(e.ref()); + } + } + } +} 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 c72da888..c951a760 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 @@ -1,49 +1,25 @@ package io.xpipe.ext.base.script; -import io.xpipe.app.comp.base.DropdownComp; -import io.xpipe.app.comp.base.StoreToggleComp; import io.xpipe.app.comp.base.SystemStateComp; -import io.xpipe.app.comp.store.*; +import io.xpipe.app.comp.store.StoreEntryWrapper; +import io.xpipe.app.comp.store.StoreViewState; import io.xpipe.app.ext.DataStoreProvider; +import io.xpipe.app.ext.EnabledStoreProvider; import io.xpipe.app.ext.GuiDialog; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.impl.DataStoreChoiceComp; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.util.OptionsBuilder; import io.xpipe.core.store.DataStore; - import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; - import lombok.SneakyThrows; import java.util.List; -public class ScriptGroupStoreProvider implements DataStoreProvider { - - @Override - public StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) { - if (sec.getWrapper().getValidity().getValue() != DataStoreEntry.Validity.COMPLETE) { - return new DenseStoreEntryComp(sec.getWrapper(), true, null); - } - - var def = StoreToggleComp.simpleToggle( - "base.isDefaultGroup", sec, s -> s.getState().isDefault(), (s, aBoolean) -> { - var state = s.getState().toBuilder().isDefault(aBoolean).build(); - s.setState(state); - }); - - var bring = StoreToggleComp.simpleToggle( - "base.bringToShells", sec, s -> s.getState().isBringToShell(), (s, aBoolean) -> { - var state = s.getState().toBuilder().bringToShell(aBoolean).build(); - s.setState(state); - }); - - var dropdown = new DropdownComp(List.of(def, bring)); - return new DenseStoreEntryComp(sec.getWrapper(), true, dropdown); - } +public class ScriptGroupStoreProvider implements EnabledStoreProvider, DataStoreProvider { @Override public Comp stateDisplay(StoreEntryWrapper w) { @@ -100,6 +76,11 @@ public class ScriptGroupStoreProvider implements DataStoreProvider { return new SimpleStringProperty(scriptStore.getDescription()); } + @Override + public String summaryString(StoreEntryWrapper wrapper) { + return "Script group"; + } + @Override public String getDisplayIconFileName(DataStore store) { return "proc:shellEnvironment_icon.svg"; 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 bb753119..f892873f 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 @@ -9,20 +9,21 @@ import io.xpipe.app.util.Validators; import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellInitCommand; import io.xpipe.core.store.DataStore; -import io.xpipe.core.store.DataStoreState; +import io.xpipe.core.store.EnabledStoreState; import io.xpipe.core.store.FileNames; import io.xpipe.core.store.StatefulDataStore; import io.xpipe.core.util.JacksonizedValue; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Singular; import lombok.experimental.SuperBuilder; -import lombok.extern.jackson.Jacksonized; import java.util.*; @SuperBuilder @Getter @AllArgsConstructor -public abstract class ScriptStore extends JacksonizedValue implements DataStore, StatefulDataStore { +public abstract class ScriptStore extends JacksonizedValue implements DataStore, StatefulDataStore { protected final DataStoreEntryRef group; @@ -32,21 +33,20 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore, protected final String description; public static ShellControl controlWithDefaultScripts(ShellControl pc) { - return controlWithScripts(pc, getDefaultInitScripts(), getDefaultBringScripts()); + return controlWithScripts(pc, getDefaultEnabledScripts()); } public static ShellControl controlWithScripts( ShellControl pc, - List> initScripts, - List> bringScripts) { + List> enabledScripts) { try { // Don't copy scripts if we don't want to modify the file system if (!pc.getEffectiveSecurityPolicy().permitTempScriptCreation()) { return pc; } - var initFlattened = flatten(initScripts); - var bringFlattened = flatten(bringScripts); + var initFlattened = flatten(enabledScripts).stream().filter(store -> store.isInitScript()).toList(); + var bringFlattened = flatten(enabledScripts).stream().filter(store -> store.isShellScript()).toList(); // Optimize if we have nothing to do if (initFlattened.isEmpty() && bringFlattened.isEmpty()) { @@ -150,18 +150,10 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore, return targetDir; } - public static List> getDefaultInitScripts() { + public static List> getDefaultEnabledScripts() { return DataStorage.get().getStoreEntries().stream() .filter(dataStoreEntry -> dataStoreEntry.getStore() instanceof ScriptStore scriptStore - && scriptStore.getState().isDefault()) - .map(DataStoreEntry::ref) - .toList(); - } - - public static List> getDefaultBringScripts() { - return DataStorage.get().getStoreEntries().stream() - .filter(dataStoreEntry -> dataStoreEntry.getStore() instanceof ScriptStore scriptStore - && scriptStore.getState().isBringToShell()) + && scriptStore.getState().isEnabled()) .map(DataStoreEntry::ref) .toList(); } @@ -194,8 +186,8 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore, } @Override - public Class getStateClass() { - return State.class; + public Class getStateClass() { + return EnabledStoreState.class; } @Override @@ -220,13 +212,4 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore, protected abstract void queryFlattenedScripts(LinkedHashSet all); public abstract List> getEffectiveScripts(); - - @Value - @EqualsAndHashCode(callSuper=true) - @SuperBuilder(toBuilder = true) - @Jacksonized - public static class State extends DataStoreState { - boolean isDefault; - boolean bringToShell; - } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptTargetType.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptTargetType.java new file mode 100644 index 00000000..0d854e2c --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptTargetType.java @@ -0,0 +1,13 @@ +package io.xpipe.ext.base.script; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public enum ScriptTargetType { + + @JsonProperty("shellInit") + SHELL_INIT, + @JsonProperty("shellSession") + SHELL_SESSION, + @JsonProperty("file") + FILE +} 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 f239e30a..bc21abd7 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 @@ -1,12 +1,14 @@ package io.xpipe.ext.base.script; import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.app.core.AppI18n; import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.util.ScriptHelper; import io.xpipe.app.util.Validators; import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellDialect; import io.xpipe.core.process.ShellInitCommand; +import io.xpipe.core.util.ValidationException; import lombok.Getter; import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; @@ -25,8 +27,11 @@ public class SimpleScriptStore extends ScriptStore implements ShellInitCommand.T private final ShellDialect minimumDialect; private final String commands; + private final boolean initScript; + private final boolean shellScript; + private final boolean fileScript; - private String assemble(ShellControl shellControl) { + public String assemble(ShellControl shellControl) { var targetType = shellControl.getOriginalShellDialect(); if (minimumDialect.isCompatibleTo(targetType)) { var shebang = commands.startsWith("#"); @@ -47,6 +52,9 @@ public class SimpleScriptStore extends ScriptStore implements ShellInitCommand.T Validators.nonNull(group); super.checkComplete(); Validators.nonNull(minimumDialect); + if (!initScript && !shellScript && !fileScript) { + throw new ValidationException(AppI18n.get("app.valueMustNotBeEmpty")); + } } public void queryFlattenedScripts(LinkedHashSet all) { 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 808f3b18..be6a104c 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 @@ -1,25 +1,25 @@ package io.xpipe.ext.base.script; -import io.xpipe.app.comp.base.DropdownComp; import io.xpipe.app.comp.base.IntegratedTextAreaComp; -import io.xpipe.app.comp.base.StoreToggleComp; +import io.xpipe.app.comp.base.ListSelectorComp; import io.xpipe.app.comp.base.SystemStateComp; -import io.xpipe.app.comp.store.*; +import io.xpipe.app.comp.store.StoreEntryWrapper; +import io.xpipe.app.comp.store.StoreViewState; import io.xpipe.app.core.AppExtensionManager; +import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.DataStoreProvider; +import io.xpipe.app.ext.EnabledParentStoreProvider; import io.xpipe.app.ext.GuiDialog; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.impl.DataStoreChoiceComp; import io.xpipe.app.fxcomps.impl.DataStoreListChoiceComp; -import io.xpipe.app.fxcomps.util.BindingsHelper; -import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.util.MarkdownBuilder; import io.xpipe.app.util.OptionsBuilder; +import io.xpipe.app.util.Validator; import io.xpipe.core.process.ShellDialect; import io.xpipe.core.store.DataStore; import io.xpipe.core.util.Identifiers; - import javafx.beans.binding.Bindings; import javafx.beans.property.Property; import javafx.beans.property.SimpleListProperty; @@ -27,16 +27,14 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; - import lombok.SneakyThrows; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; -import java.util.UUID; +import java.util.function.Function; import java.util.stream.Collectors; -public class SimpleScriptStoreProvider implements DataStoreProvider { +public class SimpleScriptStoreProvider implements EnabledParentStoreProvider, DataStoreProvider { @Override public boolean editByDefault() { @@ -48,43 +46,6 @@ public class SimpleScriptStoreProvider implements DataStoreProvider { return true; } - @Override - public StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) { - if (sec.getWrapper().getValidity().getValue() != DataStoreEntry.Validity.COMPLETE) { - return new DenseStoreEntryComp(sec.getWrapper(), true, null); - } - - var def = StoreToggleComp.simpleToggle( - "base.isDefaultGroup", sec, s -> s.getState().isDefault(), (s, aBoolean) -> { - var state = s.getState().toBuilder().isDefault(aBoolean).build(); - s.setState(state); - }); - - var bring = StoreToggleComp.simpleToggle( - "base.bringToShells", sec, s -> s.getState().isBringToShell(), (s, aBoolean) -> { - var state = s.getState().toBuilder().bringToShell(aBoolean).build(); - s.setState(state); - }); - - SimpleScriptStore s = sec.getWrapper().getEntry().getStore().asNeeded(); - var groupWrapper = StoreViewState.get().getEntryWrapper(s.getGroup().getEntry()); - - // Disable selection if parent group is already made default - def.disable(BindingsHelper.map(groupWrapper.getPersistentState(), o -> { - ScriptStore.State state = (ScriptStore.State) o; - return state.isDefault(); - })); - - // Disable selection if parent group is already brings - bring.disable(BindingsHelper.map(groupWrapper.getPersistentState(), o -> { - ScriptStore.State state = (ScriptStore.State) o; - return state.isBringToShell(); - })); - - var dropdown = new DropdownComp(List.of(def, bring)); - return new DenseStoreEntryComp(sec.getWrapper(), true, dropdown); - } - @Override public boolean shouldHaveChildren() { return false; @@ -148,6 +109,39 @@ public class SimpleScriptStoreProvider implements DataStoreProvider { "io.xpipe.ext.proc.ShellDialectChoiceComp") .getDeclaredConstructor(Property.class, boolean.class) .newInstance(dialect, false); + + var vals = List.of(0, 1, 2); + var selectedStart = new ArrayList(); + if (st.isInitScript()) { + selectedStart.add(0); + } + if (st.isShellScript()) { + selectedStart.add(1); + } + if (st.isFileScript()) { + selectedStart.add(2); + } + var name = new Function() { + + @Override + public String apply(Integer integer) { + if (integer == 0) { + return AppI18n.get("initScript"); + } + + if (integer == 1) { + return AppI18n.get("shellScript"); + } + + if (integer == 2) { + return AppI18n.get("fileScript"); + } + return "?"; + } + }; + var selectedExecTypes = new SimpleListProperty<>(FXCollections.observableList(selectedStart)); + var selectorComp = new ListSelectorComp<>(vals, name, selectedExecTypes, v -> false, false); + return new OptionsBuilder() .name("snippets") .description("snippetsDescription") @@ -174,7 +168,10 @@ public class SimpleScriptStoreProvider implements DataStoreProvider { : "sh"; })), commandProp) - .name("executionType") + .nameAndDescription("executionType") + .longDescription("base:executionType") + .addComp(selectorComp, selectedExecTypes) + .check(validator -> Validator.nonEmpty(validator, AppI18n.observable("executionType"), selectedExecTypes)) .name("scriptGroup") .description("scriptGroupDescription") .addComp( @@ -195,51 +192,15 @@ public class SimpleScriptStoreProvider implements DataStoreProvider { .scripts(new ArrayList<>(others.get())) .description(st.getDescription()) .commands(commandProp.getValue()) + .initScript(selectedExecTypes.contains(0)) + .shellScript(selectedExecTypes.contains(1)) + .fileScript(selectedExecTypes.contains(2)) .build(); }, store) .buildDialog(); } - @Override - public void init() { - DataStorage.get() - .addStoreEntryIfNotPresent(DataStoreEntry.createNew( - UUID.fromString("a9945ad2-db61-4304-97d7-5dc4330691a7"), - DataStorage.CUSTOM_SCRIPTS_CATEGORY_UUID, - "My scripts", - ScriptGroupStore.builder().build())); - - for (PredefinedScriptGroup value : PredefinedScriptGroup.values()) { - ScriptGroupStore store = ScriptGroupStore.builder() - .description(value.getDescription()) - .build(); - var e = DataStorage.get() - .addStoreEntryIfNotPresent(DataStoreEntry.createNew( - UUID.nameUUIDFromBytes(("a " + value.getName()).getBytes(StandardCharsets.UTF_8)), - DataStorage.PREDEFINED_SCRIPTS_CATEGORY_UUID, - value.getName(), - store)); - e.setStoreInternal(store, false); - e.setExpanded(value.isExpanded()); - value.setEntry(e.ref()); - } - - for (PredefinedScriptStore value : PredefinedScriptStore.values()) { - var previous = DataStorage.get().getStoreEntryIfPresent(value.getUuid()); - var store = value.getScriptStore().get(); - if (previous.isPresent()) { - previous.get().setStoreInternal(store, false); - value.setEntry(previous.get().ref()); - } else { - var e = DataStoreEntry.createNew( - value.getUuid(), DataStorage.PREDEFINED_SCRIPTS_CATEGORY_UUID, value.getName(), store); - DataStorage.get().addStoreEntryIfNotPresent(e); - value.setEntry(e.ref()); - } - } - } - @Override public ObservableValue informationString(StoreEntryWrapper wrapper) { SimpleScriptStore scriptStore = wrapper.getEntry().getStore().asNeeded(); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceGroupStore.java b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceGroupStore.java new file mode 100644 index 00000000..a3750209 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceGroupStore.java @@ -0,0 +1,24 @@ +package io.xpipe.ext.base.service; + +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.util.Validators; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.util.JacksonizedValue; +import io.xpipe.ext.base.GroupStore; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.experimental.FieldDefaults; +import lombok.experimental.SuperBuilder; + +@Getter +@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) +@SuperBuilder +public abstract class AbstractServiceGroupStore extends JacksonizedValue implements DataStore, GroupStore { + + DataStoreEntryRef parent; + + @Override + public void checkComplete() throws Throwable { + Validators.nonNull(parent); + } +} 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 new file mode 100644 index 00000000..d2d3cbb9 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceGroupStoreProvider.java @@ -0,0 +1,70 @@ +package io.xpipe.ext.base.service; + +import io.xpipe.app.comp.base.StoreToggleComp; +import io.xpipe.app.comp.base.SystemStateComp; +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.ext.DataStoreProvider; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.util.ThreadHelper; +import io.xpipe.core.store.DataStore; +import javafx.beans.binding.Bindings; +import javafx.beans.property.SimpleObjectProperty; + +public abstract class AbstractServiceGroupStoreProvider implements DataStoreProvider { + + @Override + public StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) { + var t = createToggleComp(sec); + return StoreEntryComp.create(sec.getWrapper(), t, preferLarge); + } + + private StoreToggleComp createToggleComp(StoreSection sec) { + var t = StoreToggleComp.>enableToggle(null, sec, g -> false, (g, aBoolean) -> { + var children = DataStorage.get().getStoreChildren(sec.getWrapper().getEntry()); + ThreadHelper.runFailableAsync(() -> { + for (DataStoreEntry child : children) { + if (child.getStore() instanceof AbstractServiceStore serviceStore) { + if (aBoolean) { + serviceStore.startSessionIfNeeded(); + } else { + serviceStore.stopSessionIfNeeded(); + } + } + } + }); + }); + t.setCustomVisibility(Bindings.createBooleanBinding(() -> { + var children = DataStorage.get().getStoreChildren(sec.getWrapper().getEntry()); + for (DataStoreEntry child : children) { + if (child.getStore() instanceof AbstractServiceStore serviceStore) { + if (serviceStore.getHost().getStore().requiresTunnel()) { + return true; + } + } + } + return false; + }, StoreViewState.get().getAllEntries().getList())); + return t; + } + + @Override + public Comp stateDisplay(StoreEntryWrapper w) { + return new SystemStateComp(new SimpleObjectProperty<>(SystemStateComp.State.SUCCESS)); + } + + @Override + public String getDisplayIconFileName(DataStore store) { + return "base:serviceGroup_icon.svg"; + } + + @Override + public DataStoreEntry getDisplayParent(DataStoreEntry store) { + AbstractServiceGroupStore s = store.getStore().asNeeded(); + return s.getParent().get(); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStore.java b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStore.java new file mode 100644 index 00000000..721dba40 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStore.java @@ -0,0 +1,42 @@ +package io.xpipe.ext.base.service; + +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.util.HostHelper; +import io.xpipe.app.util.Validators; +import io.xpipe.core.store.*; +import io.xpipe.core.util.JacksonizedValue; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +@SuperBuilder +@Getter +public abstract class AbstractServiceStore extends JacksonizedValue implements SingletonSessionStore, DataStore { + + public abstract DataStoreEntryRef getHost(); + + private final Integer remotePort; + private final Integer localPort; + + @Override + public void checkComplete() throws Throwable { + Validators.nonNull(getHost()); + Validators.isType(getHost(), NetworkTunnelStore.class); + Validators.nonNull(remotePort); + } + + public boolean requiresTunnel() { + return getHost().getStore().requiresTunnel(); + } + + @Override + public NetworkTunnelSession newSession() throws Exception { + ServiceLicenseCheck.check(); + var l = localPort != null ? localPort : HostHelper.findRandomOpenPortOnAllLocalInterfaces(); + return getHost().getStore().sessionChain(l, remotePort); + } + + @Override + public Class getSessionClass() { + return NetworkTunnelSession.class; + } +} 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 new file mode 100644 index 00000000..54e3db68 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/AbstractServiceStoreProvider.java @@ -0,0 +1,100 @@ +package io.xpipe.ext.base.service; + +import io.xpipe.app.comp.base.SystemStateComp; +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.ext.ActionProvider; +import io.xpipe.app.ext.DataStoreProvider; +import io.xpipe.app.ext.SingletonSessionStoreProvider; +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.core.store.DataStore; +import javafx.beans.binding.Bindings; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ObservableValue; + +import java.util.List; + +public abstract class AbstractServiceStoreProvider implements SingletonSessionStoreProvider, DataStoreProvider { + + @Override + public ActionProvider.Action launchAction(DataStoreEntry store) { + return new ActionProvider.Action() { + @Override + public void execute() throws Exception { + AbstractServiceStore s = store.getStore().asNeeded(); + s.startSessionIfNeeded(); + } + }; + } + + @Override + public DataStoreEntry getSyntheticParent(DataStoreEntry store) { + AbstractServiceStore s = store.getStore().asNeeded(); + return DataStorage.get().getOrCreateNewSyntheticEntry(s.getHost().get(), "Services", ServiceGroupStore.builder().parent(s.getHost()).build()); + } + + @Override + public Comp stateDisplay(StoreEntryWrapper w) { + return new SystemStateComp(Bindings.createObjectBinding( + () -> { + AbstractServiceStore s = w.getEntry().getStore().asNeeded(); + if (!s.requiresTunnel()) { + return SystemStateComp.State.SUCCESS; + } + + if (!s.isSessionEnabled()) { + return SystemStateComp.State.OTHER; + } + + return s.isSessionRunning() ? SystemStateComp.State.SUCCESS : SystemStateComp.State.FAILURE; + }, + w.getCache())); + } + + @Override + public StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) { + var toggle = createToggleComp(sec); + toggle.setCustomVisibility(Bindings.createBooleanBinding( + () -> { + AbstractServiceStore s = sec.getWrapper().getEntry().getStore().asNeeded(); + if (!s.getHost().getStore().requiresTunnel()) { + return false; + } + + return true; + }, + sec.getWrapper().getCache())); + return StoreEntryComp.create(sec.getWrapper(), toggle, preferLarge); + } + + @Override + public List getSearchableTerms(DataStore store) { + AbstractServiceStore s = store.asNeeded(); + return s.getLocalPort() != null ? List.of("" + s.getRemotePort(), "" + s.getLocalPort()) : List.of("" + s.getRemotePort()); + } + + @Override + public String summaryString(StoreEntryWrapper wrapper) { + AbstractServiceStore s = wrapper.getEntry().getStore().asNeeded(); + return DataStoreFormatter.toApostropheName(s.getHost().get()) + " service"; + } + + @Override + public ObservableValue informationString(StoreEntryWrapper wrapper) { + AbstractServiceStore s = wrapper.getEntry().getStore().asNeeded(); + if (s.getLocalPort() != null) { + return new SimpleStringProperty("Port " + s.getLocalPort() + " <- " + s.getRemotePort()); + } else { + return new SimpleStringProperty("Port " + s.getRemotePort()); + } + } + + @Override + public String getDisplayIconFileName(DataStore store) { + return "base:service_icon.svg"; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStore.java b/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStore.java new file mode 100644 index 00000000..12ef3488 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStore.java @@ -0,0 +1,17 @@ +package io.xpipe.ext.base.service; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.core.store.NetworkTunnelStore; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +@SuperBuilder +@Getter +@Jacksonized +@JsonTypeName("customService") +public final class CustomServiceStore extends AbstractServiceStore { + + private final DataStoreEntryRef host; +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStoreProvider.java new file mode 100644 index 00000000..45e95dcd --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/CustomServiceStoreProvider.java @@ -0,0 +1,70 @@ +package io.xpipe.ext.base.service; + +import io.xpipe.app.comp.store.StoreViewState; +import io.xpipe.app.ext.GuiDialog; +import io.xpipe.app.fxcomps.impl.DataStoreChoiceComp; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.util.OptionsBuilder; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.store.NetworkTunnelStore; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; + +import java.util.List; + +public class CustomServiceStoreProvider extends AbstractServiceStoreProvider { + + @Override + public CreationCategory getCreationCategory() { + return CreationCategory.SERVICE; + } + + @Override + public GuiDialog guiDialog(DataStoreEntry entry, Property store) { + CustomServiceStore st = store.getValue().asNeeded(); + var host = new SimpleObjectProperty<>(st.getHost()); + var localPort = new SimpleObjectProperty<>(st.getLocalPort()); + var remotePort = new SimpleObjectProperty<>(st.getRemotePort()); + + var q = new OptionsBuilder() + .nameAndDescription("serviceHost") + .addComp( + DataStoreChoiceComp.other( + host, + NetworkTunnelStore.class, + n -> n.getStore().isLocallyTunneable(), + StoreViewState.get().getAllConnectionsCategory()), + host) + .nonNull() + .nameAndDescription("serviceRemotePort") + .addInteger(remotePort) + .nonNull() + .nameAndDescription("serviceLocalPort") + .addInteger(localPort) + .bind( + () -> { + return CustomServiceStore.builder() + .host(host.get()) + .localPort(localPort.get()) + .remotePort(remotePort.get()) + .build(); + }, + store); + return q.buildDialog(); + } + + @Override + public DataStore defaultStore() { + return CustomServiceStore.builder().build(); + } + + @Override + public List getPossibleNames() { + return List.of("customService"); + } + + @Override + public List> getStoreClasses() { + return List.of(CustomServiceStore.class); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceCreatorStore.java b/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceCreatorStore.java new file mode 100644 index 00000000..05e83f1f --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceCreatorStore.java @@ -0,0 +1,11 @@ +package io.xpipe.ext.base.service; + +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.core.store.DataStore; + +import java.util.List; + +public interface FixedServiceCreatorStore extends DataStore { + + List> createFixedServices() throws Exception; +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceGroupStore.java b/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceGroupStore.java new file mode 100644 index 00000000..780c73ba --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceGroupStore.java @@ -0,0 +1,29 @@ +package io.xpipe.ext.base.service; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.util.FixedHierarchyStore; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.store.FixedChildStore; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.experimental.FieldDefaults; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; + +@Getter +@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) +@SuperBuilder +@Jacksonized +@JsonTypeName("fixedServiceGroup") +public class FixedServiceGroupStore extends AbstractServiceGroupStore implements DataStore, FixedHierarchyStore { + + @Override + @SuppressWarnings("unchecked") + public List> listChildren(DataStoreEntry self) throws Exception { + return (List>) getParent().getStore().createFixedServices(); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceGroupStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceGroupStoreProvider.java new file mode 100644 index 00000000..132b38fb --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceGroupStoreProvider.java @@ -0,0 +1,30 @@ +package io.xpipe.ext.base.service; + +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.core.store.DataStore; + +import java.util.List; + +public class FixedServiceGroupStoreProvider extends AbstractServiceGroupStoreProvider { + + @Override + public DataStore defaultStore() { + return FixedServiceGroupStore.builder().build(); + } + + @Override + public DataStoreEntry getDisplayParent(DataStoreEntry store) { + FixedServiceGroupStore s = store.getStore().asNeeded(); + return s.getParent().get(); + } + + @Override + public List getPossibleNames() { + return List.of("fixedServiceGroup"); + } + + @Override + public List> getStoreClasses() { + return List.of(FixedServiceGroupStore.class); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStore.java b/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStore.java new file mode 100644 index 00000000..aa37c2aa --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStore.java @@ -0,0 +1,32 @@ +package io.xpipe.ext.base.service; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.store.FixedChildStore; +import io.xpipe.core.store.NetworkTunnelStore; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.util.OptionalInt; + +@SuperBuilder +@Getter +@Jacksonized +@JsonTypeName("fixedService") +public class FixedServiceStore extends AbstractServiceStore implements FixedChildStore { + + private final DataStoreEntryRef host; + private final DataStoreEntryRef displayParent; + + @Override + public DataStoreEntryRef getHost() { + return host; + } + + @Override + public OptionalInt getFixedId() { + return OptionalInt.of(getRemotePort()); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStoreProvider.java new file mode 100644 index 00000000..72c67720 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/FixedServiceStoreProvider.java @@ -0,0 +1,35 @@ +package io.xpipe.ext.base.service; + +import io.xpipe.app.comp.store.StoreEntryWrapper; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ObservableValue; + +import java.util.List; + +public class FixedServiceStoreProvider extends AbstractServiceStoreProvider { + + @Override + public DataStoreEntry getSyntheticParent(DataStoreEntry store) { + FixedServiceStore s = store.getStore().asNeeded(); + return DataStorage.get().getOrCreateNewSyntheticEntry(s.getHost().get(), "Services", + FixedServiceGroupStore.builder().parent(s.getDisplayParent().get().ref()).build()); + } + + @Override + public List getPossibleNames() { + return List.of("fixedService"); + } + + @Override + public ObservableValue informationString(StoreEntryWrapper wrapper) { + FixedServiceStore s = wrapper.getEntry().getStore().asNeeded(); + return new SimpleStringProperty("Port " + s.getRemotePort()); + } + + @Override + public List> getStoreClasses() { + return List.of(FixedServiceStore.class); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/MappedServiceStore.java b/ext/base/src/main/java/io/xpipe/ext/base/service/MappedServiceStore.java new file mode 100644 index 00000000..69ab2e9f --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/MappedServiceStore.java @@ -0,0 +1,15 @@ +package io.xpipe.ext.base.service; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +@SuperBuilder +@Getter +@Jacksonized +@JsonTypeName("mappedService") +public class MappedServiceStore extends FixedServiceStore { + + private final int containerPort; +} 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 new file mode 100644 index 00000000..39ea2f8f --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/MappedServiceStoreProvider.java @@ -0,0 +1,26 @@ +package io.xpipe.ext.base.service; + +import io.xpipe.app.comp.store.StoreEntryWrapper; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ObservableValue; + +import java.util.List; + +public class MappedServiceStoreProvider extends FixedServiceStoreProvider { + + @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()); + } + + @Override + public List> getStoreClasses() { + return List.of(MappedServiceStore.class); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceCopyUrlAction.java b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceCopyUrlAction.java new file mode 100644 index 00000000..d3f35e90 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceCopyUrlAction.java @@ -0,0 +1,60 @@ +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.ClipboardHelper; +import javafx.beans.value.ObservableValue; +import lombok.Value; + +public class ServiceCopyUrlAction implements ActionProvider { + + @Override + public LeafDataStoreCallSite getLeafDataStoreCallSite() { + return new LeafDataStoreCallSite() { + + @Override + public boolean isMajor(DataStoreEntryRef o) { + return true; + } + + @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("copyUrl"); + } + + @Override + public String getIcon(DataStoreEntryRef store) { + return "mdi2c-content-copy"; + } + }; + } + + @Value + static class Action implements ActionProvider.Action { + + AbstractServiceStore serviceStore; + + @Override + public void execute() throws Exception { + serviceStore.startSessionIfNeeded(); + var l = serviceStore.getSession().getLocalPort(); + ClipboardHelper.copyUrl("localhost:" + l); + } + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceGroupStore.java b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceGroupStore.java new file mode 100644 index 00000000..22c2d8a4 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceGroupStore.java @@ -0,0 +1,28 @@ +package io.xpipe.ext.base.service; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.util.Validators; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.util.JacksonizedValue; +import io.xpipe.ext.base.GroupStore; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.experimental.FieldDefaults; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +@Getter +@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) +@SuperBuilder +@Jacksonized +@JsonTypeName("serviceGroup") +public class ServiceGroupStore extends JacksonizedValue implements DataStore, GroupStore { + + DataStoreEntryRef parent; + + @Override + public void checkComplete() throws Throwable { + Validators.nonNull(parent); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceGroupStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceGroupStoreProvider.java new file mode 100644 index 00000000..c6df32da --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceGroupStoreProvider.java @@ -0,0 +1,30 @@ +package io.xpipe.ext.base.service; + +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.core.store.DataStore; + +import java.util.List; + +public class ServiceGroupStoreProvider extends AbstractServiceGroupStoreProvider { + + @Override + public DataStore defaultStore() { + return ServiceGroupStore.builder().build(); + } + + @Override + public DataStoreEntry getDisplayParent(DataStoreEntry store) { + ServiceGroupStore s = store.getStore().asNeeded(); + return s.getParent().get(); + } + + @Override + public List getPossibleNames() { + return List.of("serviceGroup"); + } + + @Override + public List> getStoreClasses() { + return List.of(ServiceGroupStore.class); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceLicenseCheck.java b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceLicenseCheck.java new file mode 100644 index 00000000..25ce42c7 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceLicenseCheck.java @@ -0,0 +1,34 @@ +package io.xpipe.ext.base.service; + +import io.xpipe.app.util.LicenseConnectionLimit; +import io.xpipe.app.util.LicenseProvider; +import io.xpipe.app.util.LicensedFeature; +import io.xpipe.core.store.DataStore; + +public class ServiceLicenseCheck { + + public static LicensedFeature getFeature() { + return LicenseProvider.get().getFeature("services"); + } + + public static void check() { + if (getFeature().isSupported()) { + return; + } + + var limit = getConnectionLimit(); + limit.checkLimit(); + } + + + public static LicenseConnectionLimit getConnectionLimit() { + // We check before starting a new service + return new LicenseConnectionLimit(0, getFeature()) { + + @Override + protected boolean matches(DataStore store) { + return store instanceof AbstractServiceStore abstractServiceStore && abstractServiceStore.requiresTunnel() && abstractServiceStore.isSessionRunning(); + } + }; + } +} 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 new file mode 100644 index 00000000..ca122c55 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/service/ServiceOpenAction.java @@ -0,0 +1,60 @@ +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 ServiceOpenAction implements ActionProvider { + + @Override + public LeafDataStoreCallSite getLeafDataStoreCallSite() { + return new LeafDataStoreCallSite() { + + @Override + public boolean isMajor(DataStoreEntryRef o) { + return true; + } + + @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("openWebsite"); + } + + @Override + public String getIcon(DataStoreEntryRef store) { + return "mdi2s-search-web"; + } + }; + } + + @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/store/StorePauseAction.java b/ext/base/src/main/java/io/xpipe/ext/base/store/StorePauseAction.java index 27a2cf3c..55c67c1c 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/store/StorePauseAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/store/StorePauseAction.java @@ -11,8 +11,8 @@ import lombok.Value; public class StorePauseAction implements ActionProvider { @Override - public DataStoreCallSite getDataStoreCallSite() { - return new DataStoreCallSite() { + public LeafDataStoreCallSite getLeafDataStoreCallSite() { + return new LeafDataStoreCallSite() { @Override public ActionProvider.Action createAction(DataStoreEntryRef store) { @@ -24,11 +24,6 @@ public class StorePauseAction implements ActionProvider { return PauseableStore.class; } - @Override - public boolean isMajor(DataStoreEntryRef o) { - return true; - } - @Override public ObservableValue getName(DataStoreEntryRef store) { return AppI18n.observable("pause"); @@ -46,11 +41,6 @@ public class StorePauseAction implements ActionProvider { DataStoreEntryRef entry; - @Override - public boolean requiresJavaFXPlatform() { - return false; - } - @Override public void execute() throws Exception { entry.getStore().pause(); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/store/StoreStartAction.java b/ext/base/src/main/java/io/xpipe/ext/base/store/StoreStartAction.java index db645c63..27afbe50 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/store/StoreStartAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/store/StoreStartAction.java @@ -11,8 +11,8 @@ import lombok.Value; public class StoreStartAction implements ActionProvider { @Override - public DataStoreCallSite getDataStoreCallSite() { - return new DataStoreCallSite() { + public LeafDataStoreCallSite getLeafDataStoreCallSite() { + return new LeafDataStoreCallSite() { @Override public ActionProvider.Action createAction(DataStoreEntryRef store) { @@ -24,11 +24,6 @@ public class StoreStartAction implements ActionProvider { return StartableStore.class; } - @Override - public boolean isMajor(DataStoreEntryRef o) { - return true; - } - @Override public ObservableValue getName(DataStoreEntryRef store) { return AppI18n.observable("start"); @@ -46,11 +41,6 @@ public class StoreStartAction implements ActionProvider { DataStoreEntryRef entry; - @Override - public boolean requiresJavaFXPlatform() { - return false; - } - @Override public void execute() throws Exception { entry.getStore().start(); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/store/StoreStopAction.java b/ext/base/src/main/java/io/xpipe/ext/base/store/StoreStopAction.java index 1b8392ed..899ea1d7 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/store/StoreStopAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/store/StoreStopAction.java @@ -11,8 +11,8 @@ import lombok.Value; public class StoreStopAction implements ActionProvider { @Override - public DataStoreCallSite getDataStoreCallSite() { - return new DataStoreCallSite() { + public LeafDataStoreCallSite getLeafDataStoreCallSite() { + return new LeafDataStoreCallSite() { @Override public ActionProvider.Action createAction(DataStoreEntryRef store) { @@ -24,11 +24,6 @@ public class StoreStopAction implements ActionProvider { return StoppableStore.class; } - @Override - public boolean isMajor(DataStoreEntryRef o) { - return true; - } - @Override public ObservableValue getName(DataStoreEntryRef store) { return AppI18n.observable("stop"); @@ -46,11 +41,6 @@ public class StoreStopAction implements ActionProvider { DataStoreEntryRef entry; - @Override - public boolean requiresJavaFXPlatform() { - return false; - } - @Override public void execute() throws Exception { entry.getStore().stop(); diff --git a/ext/base/src/main/java/module-info.java b/ext/base/src/main/java/module-info.java index 447e8f0a..c5b541e9 100644 --- a/ext/base/src/main/java/module-info.java +++ b/ext/base/src/main/java/module-info.java @@ -1,16 +1,17 @@ import io.xpipe.app.browser.action.BrowserAction; import io.xpipe.app.ext.ActionProvider; +import io.xpipe.app.ext.DataStorageExtensionProvider; import io.xpipe.app.ext.DataStoreProvider; import io.xpipe.ext.base.action.*; import io.xpipe.ext.base.browser.*; import io.xpipe.ext.base.desktop.DesktopApplicationStoreProvider; import io.xpipe.ext.base.desktop.DesktopCommandStoreProvider; import io.xpipe.ext.base.desktop.DesktopEnvironmentStoreProvider; +import io.xpipe.ext.base.script.RunScriptAction; +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.store.StorePauseAction; -import io.xpipe.ext.base.store.StoreStartAction; -import io.xpipe.ext.base.store.StoreStopAction; +import io.xpipe.ext.base.service.*; open module io.xpipe.ext.base { exports io.xpipe.ext.base; @@ -18,10 +19,12 @@ open module io.xpipe.ext.base { exports io.xpipe.ext.base.script; exports io.xpipe.ext.base.store; exports io.xpipe.ext.base.desktop; + exports io.xpipe.ext.base.service; requires java.desktop; requires io.xpipe.core; requires com.fasterxml.jackson.databind; + requires com.fasterxml.jackson.annotation; requires java.net.http; requires static lombok; requires static javafx.controls; @@ -30,7 +33,7 @@ open module io.xpipe.ext.base { requires org.kordamp.ikonli.javafx; requires atlantafx.base; - provides BrowserAction with + provides BrowserAction with RunScriptAction, FollowLinkAction, BackAction, ForwardAction, @@ -55,22 +58,11 @@ open module io.xpipe.ext.base { UnzipAction, JavapAction, JarAction; - provides ActionProvider with - StoreStopAction, - StoreStartAction, - StorePauseAction, - CloneStoreAction, - RefreshStoreChildrenAction, - ScanAction, - LaunchAction, + provides ActionProvider with ServiceOpenAction, ServiceCopyUrlAction, + CloneStoreAction, RefreshChildrenStoreAction, LaunchStoreAction, XPipeUrlAction, - EditStoreAction, - DeleteStoreChildrenAction, - BrowseStoreAction; - provides DataStoreProvider with - SimpleScriptStoreProvider, - DesktopEnvironmentStoreProvider, - DesktopApplicationStoreProvider, - DesktopCommandStoreProvider, - ScriptGroupStoreProvider; + EditStoreAction, DeleteChildrenStoreAction, + BrowseStoreAction, ScanStoreAction; + provides DataStoreProvider with FixedServiceGroupStoreProvider, ServiceGroupStoreProvider, CustomServiceStoreProvider, MappedServiceStoreProvider, FixedServiceStoreProvider, SimpleScriptStoreProvider, DesktopEnvironmentStoreProvider, DesktopApplicationStoreProvider, DesktopCommandStoreProvider, ScriptGroupStoreProvider; + provides DataStorageExtensionProvider with ScriptDataStorageProvider; } diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-16-dark.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-16-dark.png new file mode 100644 index 00000000..0c175e65 Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-16-dark.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-16.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-16.png new file mode 100644 index 00000000..1f2adcbd Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-16.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-24-dark.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-24-dark.png new file mode 100644 index 00000000..ee50e717 Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-24-dark.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-24.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-24.png new file mode 100644 index 00000000..6bf7ae2a Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-24.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-40-dark.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-40-dark.png new file mode 100644 index 00000000..2c97a68b Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-40-dark.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-40.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-40.png new file mode 100644 index 00000000..4a2d36ef Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-40.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-dark.svg b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-dark.svg new file mode 100644 index 00000000..cef212aa --- /dev/null +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon-dark.svg @@ -0,0 +1,127 @@ + + + + + + + ssh draft + + + + + ssh draft + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon.svg b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon.svg new file mode 100644 index 00000000..d838e990 --- /dev/null +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/serviceGroup_icon.svg @@ -0,0 +1,130 @@ + + + + + + + ssh draft + + + + + ssh draft + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-16-dark.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-16-dark.png new file mode 100644 index 00000000..9bfa541e Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-16-dark.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-16.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-16.png new file mode 100644 index 00000000..9bfa541e Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-16.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-24-dark.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-24-dark.png new file mode 100644 index 00000000..74eeff8a Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-24-dark.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-24.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-24.png new file mode 100644 index 00000000..74eeff8a Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-24.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-40-dark.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-40-dark.png new file mode 100644 index 00000000..fcf4083c Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-40-dark.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-40.png b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-40.png new file mode 100644 index 00000000..fcf4083c Binary files /dev/null and b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-40.png differ diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-dark.svg b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-dark.svg new file mode 100644 index 00000000..9ef40b7c --- /dev/null +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon-dark.svg @@ -0,0 +1,100 @@ + + + + + + + ssh draft + + + + ssh draft + + + + + + + + + + + + + + + + diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon.svg b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon.svg new file mode 100644 index 00000000..f13ea71f --- /dev/null +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/service_icon.svg @@ -0,0 +1,100 @@ + + + + + + + ssh draft + + + + ssh draft + + + + + + + + + + + + + + + + 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 new file mode 100644 index 00000000..3e5dd771 --- /dev/null +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/apt_update.sh @@ -0,0 +1 @@ +sudo apt update \ No newline at end of file diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/crlf_to_lf.sh b/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/crlf_to_lf.sh new file mode 100644 index 00000000..585611c9 --- /dev/null +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts/crlf_to_lf.sh @@ -0,0 +1,8 @@ +for arg in "$@" +do + file="$arg" + temp_file=$(mktemp) + awk '{ sub("\r$", ""); print }' "$file" > "$temp_file" + cat "$temp_file" > "$file" + rm "$temp_file" +done diff --git a/gradle/gradle_scripts/atlantafx-base-2.0.2.jar b/gradle/gradle_scripts/atlantafx-base-2.0.2.jar new file mode 100644 index 00000000..6cb3a389 Binary files /dev/null and b/gradle/gradle_scripts/atlantafx-base-2.0.2.jar differ diff --git a/gradle/gradle_scripts/dev_default.properties b/gradle/gradle_scripts/dev_default.properties index f12fd39e..cc12f335 100644 --- a/gradle/gradle_scripts/dev_default.properties +++ b/gradle/gradle_scripts/dev_default.properties @@ -12,3 +12,6 @@ io.xpipe.app.showcase=false # Location in which your local development connection should be stored. If left empty, it will use your global XPipe storage in ~/.xpipe. io.xpipe.app.dataDir=local + +# When enabled, all http server input and output is printed. Useful for debugging +io.xpipe.beacon.printMessages=false diff --git a/gradle/gradle_scripts/java.gradle b/gradle/gradle_scripts/java.gradle index ee14af1d..a42f806f 100644 --- a/gradle/gradle_scripts/java.gradle +++ b/gradle/gradle_scripts/java.gradle @@ -22,7 +22,7 @@ javadoc{ addStringOption('link', 'https://docs.oracle.com/en/java/javase/21/docs/api/') addBooleanOption('html5', true) addStringOption('Xdoclint:none', '-quiet') - addBooleanOption('-enable-preview', true) + // addBooleanOption('-enable-preview', true) } } diff --git a/gradle/gradle_scripts/local_junit_suite.gradle b/gradle/gradle_scripts/local_junit_suite.gradle index a8340811..989adaee 100644 --- a/gradle/gradle_scripts/local_junit_suite.gradle +++ b/gradle/gradle_scripts/local_junit_suite.gradle @@ -24,7 +24,7 @@ testing { systemProperty 'io.xpipe.app.fullVersion', "true" systemProperty 'io.xpipe.beacon.printDaemonOutput', "false" systemProperty 'io.xpipe.app.useVirtualThreads', "false" - // systemProperty "io.xpipe.beacon.port", "21725" + systemProperty "io.xpipe.beacon.port", "21723" systemProperty "io.xpipe.beacon.launchDebugDaemon", "true" systemProperty "io.xpipe.app.dataDir", "$projectDir/local/" systemProperty "io.xpipe.app.logLevel", "trace" diff --git a/gradle/gradle_scripts/modules.gradle b/gradle/gradle_scripts/modules.gradle index 1b6bb40e..abecf48a 100644 --- a/gradle/gradle_scripts/modules.gradle +++ b/gradle/gradle_scripts/modules.gradle @@ -24,12 +24,6 @@ extraJavaModuleInfo { } } -extraJavaModuleInfo { - module("org.ocpsoft.prettytime:prettytime", "org.ocpsoft.prettytime") { - exportAllPackages() - } -} - extraJavaModuleInfo { module("com.vladsch.flexmark:flexmark", "com.vladsch.flexmark") { mergeJar('com.vladsch.flexmark:flexmark-util') @@ -50,6 +44,8 @@ extraJavaModuleInfo { mergeJar('com.vladsch.flexmark:flexmark-ext-footnotes') mergeJar('com.vladsch.flexmark:flexmark-ext-definition') mergeJar('com.vladsch.flexmark:flexmark-ext-anchorlink') + mergeJar('com.vladsch.flexmark:flexmark-ext-yaml-front-matter') + mergeJar('com.vladsch.flexmark:flexmark-ext-toc') exportAllPackages() } } diff --git a/gradle/gradle_scripts/remote_junit_suite.gradle b/gradle/gradle_scripts/remote_junit_suite.gradle index 6df6e012..d6a9844f 100644 --- a/gradle/gradle_scripts/remote_junit_suite.gradle +++ b/gradle/gradle_scripts/remote_junit_suite.gradle @@ -27,7 +27,7 @@ testing { systemProperty "io.xpipe.beacon.customDaemonCommand", "\"$rootDir/gradlew\" --console=plain $daemonCommand" } systemProperty "io.xpipe.beacon.daemonArgs", - " -Dio.xpipe.beacon.port=21725" + + " -Dio.xpipe.beacon.port=21723" + " -Dio.xpipe.app.dataDir=$projectDir/local/" + " -Dio.xpipe.storage.persist=false" + " -Dio.xpipe.app.writeSysOut=true" + @@ -36,7 +36,7 @@ testing { " -Dio.xpipe.app.logLevel=trace" systemProperty 'io.xpipe.beacon.printDaemonOutput', "true" - systemProperty "io.xpipe.beacon.port", "21725" + systemProperty "io.xpipe.beacon.port", "21723" systemProperty "io.xpipe.beacon.launchDebugDaemon", "true" } } diff --git a/gradle/gradle_scripts/vernacular-1.16.jar b/gradle/gradle_scripts/vernacular-1.16.jar index 062d3b27..261aaa8f 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 510f28ae..6f7a6eb3 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-rc-1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/lang/app/strings/translations_da.properties b/lang/app/strings/translations_da.properties index 6ef13280..73bc007b 100644 --- a/lang/app/strings/translations_da.properties +++ b/lang/app/strings/translations_da.properties @@ -25,6 +25,7 @@ moveTo=Flyt til ... addDatabase=Database ... browseInternalStorage=Gennemse internt lager addTunnel=Tunnel ... +addService=Service ... addScript=Script ... addHost=Fjernvært ... addShell=Shell-miljø ... @@ -460,3 +461,20 @@ addNotes=Tilføj noter order=Bestille ... stickToTop=Hold dig på toppen orderAheadOf=Bestil på forhånd ... +httpServer=HTTP-server +httpServerConfiguration=Konfiguration af HTTP-server +httpServerPort=Port +httpServerPortDescription=Den port, som HTTP-serveren vil lytte på.\n\nBemærk, at hvis du ændrer dette, skal alle andre programmer, der interagerer med serveren, også konfigureres til at bruge den nye port.\n\nKræver en genstart for at gælde. +apiKey=API-nøgle +apiKeyDescription=API-nøglen til godkendelse af XPipe-dæmonens API-anmodninger. Se den generelle API-dokumentation for at få flere oplysninger om, hvordan du godkender.\n\nKræver en genstart for at blive anvendt. +disableApiAuthentication=Deaktiver API-godkendelse +disableApiAuthenticationDescription=Deaktiverer alle nødvendige godkendelsesmetoder, så enhver uautoriseret anmodning vil blive håndteret.\n\nAutentificering bør kun deaktiveres til udviklingsformål.\n\nKræver en genstart for at blive anvendt. +api=API +storeIntroImportDescription=Bruger du allerede XPipe på et andet system? Synkroniser dine eksisterende forbindelser på tværs af flere systemer via et eksternt git-repository. Du kan også synkronisere senere når som helst, hvis det ikke er sat op endnu. +importConnections=Synkroniser forbindelser +importConnectionsTitle=Importer forbindelser +showAllChildren=Vis alle børn +httpApi=HTTP API +isOnlySupportedLimit=understøttes kun med en professionel licens, når man har mere end $COUNT$ forbindelser +areOnlySupportedLimit=understøttes kun med en professionel licens, når man har mere end $COUNT$ forbindelser +enabled=Aktiveret diff --git a/lang/app/strings/translations_de.properties b/lang/app/strings/translations_de.properties index 50229dae..0349fcfd 100644 --- a/lang/app/strings/translations_de.properties +++ b/lang/app/strings/translations_de.properties @@ -25,6 +25,7 @@ moveTo=Kategorie ändern ... addDatabase=Datenbank ... browseInternalStorage=Internen Speicher durchsuchen addTunnel=Tunnel ... +addService=Service ... addScript=Skript ... #custom addHost=Remote Host ... @@ -454,3 +455,20 @@ addNotes=Notizen hinzufügen order=Bestellen ... stickToTop=Oben bleiben orderAheadOf=Vorbestellen ... +httpServer=HTTP-Server +httpServerConfiguration=HTTP-Server-Konfiguration +httpServerPort=Port +httpServerPortDescription=Der Port, auf dem der HTTP-Server lauschen wird.\n\nWenn du diesen Wert änderst, müssen alle anderen Anwendungen, die mit dem Server interagieren, so konfiguriert werden, dass sie ebenfalls den neuen Port verwenden.\n\nZur Anwendung ist ein Neustart erforderlich. +apiKey=API-Schlüssel +apiKeyDescription=Der API-Schlüssel zur Authentifizierung von XPipe Daemon API-Anfragen. Weitere Informationen zur Authentifizierung findest du in der allgemeinen API-Dokumentation.\n\nErfordert einen Neustart zur Anwendung. +disableApiAuthentication=API-Authentifizierung deaktivieren +disableApiAuthenticationDescription=Deaktiviert alle erforderlichen Authentifizierungsmethoden, so dass jede nicht authentifizierte Anfrage bearbeitet wird.\n\nDie Authentifizierung sollte nur zu Entwicklungszwecken deaktiviert werden.\n\nErfordert einen Neustart zur Anwendung. +api=API +storeIntroImportDescription=Benutzt du XPipe bereits auf einem anderen System? Synchronisiere deine bestehenden Verbindungen über mehrere Systeme hinweg über ein Remote-Git-Repository. Du kannst auch später jederzeit synchronisieren, wenn es noch nicht eingerichtet ist. +importConnections=Synchronisierte Verbindungen +importConnectionsTitle=Verbindungen importieren +showAllChildren=Alle Kinder anzeigen +httpApi=HTTP-API +isOnlySupportedLimit=wird nur mit einer professionellen Lizenz unterstützt, wenn mehr als $COUNT$ Verbindungen bestehen +areOnlySupportedLimit=werden nur mit einer professionellen Lizenz unterstützt, wenn mehr als $COUNT$ Verbindungen bestehen +enabled=Aktiviert diff --git a/lang/app/strings/translations_en.properties b/lang/app/strings/translations_en.properties index 6d5fbbda..8399271e 100644 --- a/lang/app/strings/translations_en.properties +++ b/lang/app/strings/translations_en.properties @@ -25,6 +25,7 @@ moveTo=Move to ... addDatabase=Database ... browseInternalStorage=Browse internal storage addTunnel=Tunnel ... +addService=Service ... addScript=Script ... addHost=Remote Host ... addShell=Shell Environment ... @@ -41,7 +42,7 @@ selectShellTypeDescription=Select the Type of the Shell Connection name=Name storeIntroTitle=Connection Hub storeIntroDescription=Here you can manage all your local and remote shell connections in one place. To start off, you can quickly detect available connections automatically and choose which ones to add. -detectConnections=Search for connections +detectConnections=Search for connections ... configuration=Configuration dragAndDropFilesHere=Or just drag and drop a file here confirmDsCreationAbortTitle=Confirm abort @@ -458,3 +459,23 @@ addNotes=Add notes order=Order ... stickToTop=Keep on top orderAheadOf=Order ahead of ... +httpServer=HTTP server +httpServerConfiguration=HTTP server configuration +#context: networking +httpServerPort=Port +#context: networking +httpServerPortDescription=The port on which the HTTP server will listen on.\n\nNote that if you change this, any other applications that interact with the server need to be configured to use the new port as well.\n\nRequires a restart to apply. +apiKey=API key +apiKeyDescription=The API key to authenticate XPipe daemon API requests. For more information on how to authenticate, see the general API documentation.\n\nRequires a restart to apply. +disableApiAuthentication=Disable API authentication +disableApiAuthenticationDescription=Disables all required authentication methods so that any unauthenticated request will be handled.\n\nAuthentication should only be disabled for development purposes.\n\nRequires a restart to apply. +api=API +storeIntroImportDescription=Already using XPipe on another system? Synchronize your existing connections across multiple systems through a remote git repository. You can also sync later at any time if it is not set up yet. +importConnections=Sync connections ... +importConnectionsTitle=Import Connections +showAllChildren=Show all children +httpApi=HTTP API +isOnlySupportedLimit=is only supported with a professional license when having more than $COUNT$ connections +areOnlySupportedLimit=are only supported with a professional license when having more than $COUNT$ connections +enabled=Enabled +enableGitStoragePtbDisabled=Git synchronization is disabled for public test builds to prevent usage with regular release git repositories and to discourage using a PTB build as your daily driver. diff --git a/lang/app/strings/translations_es.properties b/lang/app/strings/translations_es.properties index b93169c4..0fd80ce3 100644 --- a/lang/app/strings/translations_es.properties +++ b/lang/app/strings/translations_es.properties @@ -24,6 +24,7 @@ moveTo=Pasar a ... addDatabase=Base de datos ... browseInternalStorage=Explorar el almacenamiento interno addTunnel=Túnel ... +addService=Servicio ... addScript=Script ... addHost=Host remoto ... addShell=Entorno Shell ... @@ -441,3 +442,20 @@ addNotes=Añadir notas order=Ordenar ... stickToTop=Mantener arriba orderAheadOf=Haz tu pedido antes de ... +httpServer=Servidor HTTP +httpServerConfiguration=Configuración del servidor HTTP +httpServerPort=Puerto +httpServerPortDescription=El puerto en el que escuchará el servidor HTTP.\n\nTen en cuenta que si cambias esto, cualquier otra aplicación que interactúe con el servidor deberá configurarse para utilizar también el nuevo puerto.\n\nRequiere un reinicio para aplicarse. +apiKey=Clave API +apiKeyDescription=La clave API para autenticar las peticiones API del demonio XPipe. Para más información sobre cómo autenticarse, consulta la documentación general de la API.\n\nRequiere un reinicio para aplicarse. +disableApiAuthentication=Desactivar la autenticación de la API +disableApiAuthenticationDescription=Desactiva todos los métodos de autenticación requeridos para que se gestione cualquier solicitud no autenticada.\n\nLa autenticación sólo debe desactivarse con fines de desarrollo.\n\nRequiere un reinicio para aplicarse. +api=API +storeIntroImportDescription=¿Ya utilizas XPipe en otro sistema? Sincroniza tus conexiones existentes en varios sistemas a través de un repositorio git remoto. También puedes sincronizar más tarde en cualquier momento si aún no está configurado. +importConnections=Sincronizar conexiones +importConnectionsTitle=Importar conexiones +showAllChildren=Mostrar todos los niños +httpApi=API HTTP +isOnlySupportedLimit=sólo es compatible con una licencia profesional cuando tiene más de $COUNT$ conexiones +areOnlySupportedLimit=sólo son compatibles con una licencia profesional cuando tienen más de $COUNT$ conexiones +enabled=Activado diff --git a/lang/app/strings/translations_fr.properties b/lang/app/strings/translations_fr.properties index 1c88ecab..23a13e41 100644 --- a/lang/app/strings/translations_fr.properties +++ b/lang/app/strings/translations_fr.properties @@ -24,6 +24,7 @@ moveTo=Déplacer vers ... addDatabase=Base de données ... browseInternalStorage=Parcourir la mémoire interne addTunnel=Tunnel ... +addService=Service ... addScript=Script ... addHost=Hôte distant ... addShell=Environnement Shell ... @@ -441,3 +442,20 @@ addNotes=Ajouter des notes order=Commander... stickToTop=Garde le dessus orderAheadOf=Commande en avance... +httpServer=Serveur HTTP +httpServerConfiguration=Configuration du serveur HTTP +httpServerPort=Port +httpServerPortDescription=Le port sur lequel le serveur HTTP écoutera.\n\nNote que si tu modifies ce paramètre, toutes les autres applications qui interagissent avec le serveur doivent être configurées pour utiliser également le nouveau port.\n\nIl faut redémarrer l'ordinateur pour l'appliquer. +apiKey=Clé API +apiKeyDescription=La clé API pour authentifier les demandes API du démon XPipe. Pour plus d'informations sur la manière de s'authentifier, voir la documentation générale de l'API.\n\nNécessite un redémarrage pour être appliquée. +disableApiAuthentication=Désactiver l'authentification de l'API +disableApiAuthenticationDescription=Désactive toutes les méthodes d'authentification requises, de sorte que toute demande non authentifiée sera traitée.\n\nL'authentification ne doit être désactivée qu'à des fins de développement.\n\nNécessite un redémarrage pour être appliqué. +api=API +storeIntroImportDescription=Tu utilises déjà XPipe sur un autre système ? Synchronise tes connexions existantes sur plusieurs systèmes grâce à un dépôt git distant. Tu peux aussi synchroniser plus tard à tout moment si ce n'est pas encore configuré. +importConnections=Synchronisation des connexions +importConnectionsTitle=Importer des connexions +showAllChildren=Afficher tous les enfants +httpApi=API HTTP +isOnlySupportedLimit=n'est pris en charge qu'avec une licence professionnelle lorsqu'il y a plus de $COUNT$ connexions +areOnlySupportedLimit=ne sont pris en charge qu'avec une licence professionnelle lorsqu'il y a plus de $COUNT$ connexions +enabled=Activé diff --git a/lang/app/strings/translations_it.properties b/lang/app/strings/translations_it.properties index 3f4d5ddb..e03fa1dc 100644 --- a/lang/app/strings/translations_it.properties +++ b/lang/app/strings/translations_it.properties @@ -24,6 +24,7 @@ moveTo=Passare a ... addDatabase=Database ... browseInternalStorage=Sfogliare la memoria interna addTunnel=Tunnel ... +addService=Servizio ... addScript=Script ... addHost=Host remoto ... addShell=Ambiente Shell ... @@ -441,3 +442,20 @@ addNotes=Aggiungi note order=Ordinare ... stickToTop=Continua a essere in cima orderAheadOf=Ordina prima di ... +httpServer=Server HTTP +httpServerConfiguration=Configurazione del server HTTP +httpServerPort=Porta +httpServerPortDescription=La porta su cui il server HTTP si metterà in ascolto.\n\nSe la modifichi, tutte le altre applicazioni che interagiscono con il server devono essere configurate per utilizzare la nuova porta.\n\nRichiede un riavvio per essere applicata. +apiKey=Chiave API +apiKeyDescription=La chiave API per autenticare le richieste API del demone XPipe. Per ulteriori informazioni sulle modalità di autenticazione, consulta la documentazione generale dell'API.\n\nRichiede un riavvio per essere applicata. +disableApiAuthentication=Disabilita l'autenticazione API +disableApiAuthenticationDescription=Disabilita tutti i metodi di autenticazione richiesti in modo che qualsiasi richiesta non autenticata venga gestita.\n\nL'autenticazione dovrebbe essere disabilitata solo per scopi di sviluppo.\n\nRichiede un riavvio per essere applicata. +api=API +storeIntroImportDescription=Stai già usando XPipe su un altro sistema? Sincronizza le connessioni esistenti su più sistemi attraverso un repository git remoto. Puoi anche sincronizzare in seguito, in qualsiasi momento, se non è ancora stato configurato. +importConnections=Connessioni di sincronizzazione +importConnectionsTitle=Importazione di connessioni +showAllChildren=Mostra tutti i bambini +httpApi=API HTTP +isOnlySupportedLimit=è supportato solo con una licenza professionale quando ci sono più di $COUNT$ connessioni +areOnlySupportedLimit=sono supportati solo con una licenza professionale quando ci sono più di $COUNT$ connessioni +enabled=Abilitato diff --git a/lang/app/strings/translations_ja.properties b/lang/app/strings/translations_ja.properties index 79f1ae8f..b4760573 100644 --- a/lang/app/strings/translations_ja.properties +++ b/lang/app/strings/translations_ja.properties @@ -24,6 +24,7 @@ moveTo=移動する addDatabase=データベース ... browseInternalStorage=内部ストレージをブラウズする addTunnel=トンネル ... +addService=サービス ... addScript=スクリプト ... addHost=リモートホスト ... addShell=シェル環境 ... @@ -441,3 +442,20 @@ addNotes=メモを追加する order=注文する stickToTop=トップをキープする orderAheadOf=先に注文する +httpServer=HTTPサーバー +httpServerConfiguration=HTTPサーバーの設定 +httpServerPort=ポート +httpServerPortDescription=HTTPサーバーがリッスンするポート。\n\nこれを変更すると、サーバーとやりとりする他のアプリケーションも新しいポートを使うように設定する必要があることに注意。\n\n適用するには再起動が必要である。 +apiKey=APIキー +apiKeyDescription=XPipeデーモンAPIリクエストを認証するためのAPIキー。認証方法の詳細については、一般的なAPIドキュメントを参照のこと。\n\n適用には再起動が必要。 +disableApiAuthentication=API認証を無効にする +disableApiAuthenticationDescription=認証されていないリクエストが処理されるように、必要な認証方法をすべて無効にする。\n\n認証は開発目的でのみ無効にすべきである。\n\n適用するには再起動が必要である。 +api=API +storeIntroImportDescription=すでに他のシステムでXPipeを使っている?リモートgitリポジトリを通して、複数のシステム間で既存の接続を同期する。まだ設定されていない場合は、後からいつでも同期することもできる。 +importConnections=シンク接続 +importConnectionsTitle=コネクションのインポート +showAllChildren=すべての子供を表示する +httpApi=HTTP API +isOnlySupportedLimit=は、$COUNT$ を超える接続がある場合、プロフェッショナルライセンスでのみサポートされる。 +areOnlySupportedLimit=$COUNT$ 以上の接続がある場合、プロフェッショナルライセンスでのみサポートされる。 +enabled=有効にする diff --git a/lang/app/strings/translations_nl.properties b/lang/app/strings/translations_nl.properties index 9236fd5e..efa21afb 100644 --- a/lang/app/strings/translations_nl.properties +++ b/lang/app/strings/translations_nl.properties @@ -24,6 +24,7 @@ moveTo=Naar ... addDatabase=Databank ... browseInternalStorage=Bladeren door interne opslag addTunnel=Tunnel ... +addService=Service ... addScript=Script ... addHost=Externe host ... addShell=Shell-omgeving ... @@ -441,3 +442,20 @@ addNotes=Opmerkingen toevoegen order=Bestellen ... stickToTop=Bovenaan houden orderAheadOf=Vooruitbestellen ... +httpServer=HTTP-server +httpServerConfiguration=HTTP-server configuratie +httpServerPort=Poort +httpServerPortDescription=De poort waarop de HTTP-server zal luisteren.\n\nMerk op dat als je dit verandert, andere applicaties die communiceren met de server ook geconfigureerd moeten worden om de nieuwe poort te gebruiken.\n\nVereist een herstart om toe te passen. +apiKey=API-sleutel +apiKeyDescription=De API sleutel om XPipe daemon API verzoeken te authenticeren. Voor meer informatie over hoe te authenticeren, zie de algemene API documentatie.\n\nVereist een herstart om toe te passen. +disableApiAuthentication=API-authenticatie uitschakelen +disableApiAuthenticationDescription=Schakelt alle vereiste authenticatiemethoden uit, zodat elk niet-geauthenticeerd verzoek wordt afgehandeld.\n\nAuthenticatie zou alleen uitgeschakeld moeten worden voor ontwikkelingsdoeleinden.\n\nVereist een herstart om toe te passen. +api=API +storeIntroImportDescription=Gebruik je XPipe al op een ander systeem? Synchroniseer je bestaande verbindingen over meerdere systemen via een remote git repository. Je kunt ook later synchroniseren op elk gewenst moment als het nog niet is ingesteld. +importConnections=Synchronisatieverbindingen +importConnectionsTitle=Verbindingen importeren +showAllChildren=Toon alle kinderen +httpApi=HTTP API +isOnlySupportedLimit=wordt alleen ondersteund met een professionele licentie bij meer dan $COUNT$ verbindingen +areOnlySupportedLimit=worden alleen ondersteund met een professionele licentie bij meer dan $COUNT$ verbindingen +enabled=Ingeschakeld diff --git a/lang/app/strings/translations_pt.properties b/lang/app/strings/translations_pt.properties index ae0171a7..63879d58 100644 --- a/lang/app/strings/translations_pt.properties +++ b/lang/app/strings/translations_pt.properties @@ -24,6 +24,7 @@ moveTo=Move-te para ... addDatabase=Base de dados ... browseInternalStorage=Navega no armazenamento interno addTunnel=Túnel ... +addService=Serviço ... addScript=Script ... addHost=Anfitrião remoto ... addShell=Ambiente Shell ... @@ -441,3 +442,20 @@ addNotes=Adiciona notas order=Encomenda ... stickToTop=Mantém-te no topo orderAheadOf=Encomenda antes de ... +httpServer=Servidor HTTP +httpServerConfiguration=Configuração do servidor HTTP +httpServerPort=Porta +httpServerPortDescription=A porta em que o servidor HTTP irá escutar.\n\nTem em atenção que, se alterares isto, quaisquer outras aplicações que interajam com o servidor têm de ser configuradas para utilizar também a nova porta.\n\nRequer uma reinicialização para ser aplicado. +apiKey=Chave API +apiKeyDescription=A chave da API para autenticar os pedidos de API do daemon XPipe. Para mais informações sobre como autenticar, vê a documentação geral da API.\n\nRequer um reinício para ser aplicado. +disableApiAuthentication=Desativar a autenticação da API +disableApiAuthenticationDescription=Desactiva todos os métodos de autenticação necessários para que qualquer pedido não autenticado seja tratado.\n\nA autenticação só deve ser desactivada para fins de desenvolvimento.\n\nRequer um reinício para ser aplicado. +api=API +storeIntroImportDescription=Já estás a utilizar o XPipe noutro sistema? Sincroniza as tuas ligações existentes em vários sistemas através de um repositório git remoto. Também podes sincronizar mais tarde, a qualquer momento, se ainda não estiver configurado. +importConnections=Sincroniza ligações +importConnectionsTitle=Importar ligações +showAllChildren=Mostra todas as crianças +httpApi=API HTTP +isOnlySupportedLimit=só é suportado com uma licença profissional se tiver mais de $COUNT$ ligações +areOnlySupportedLimit=só são suportados com uma licença profissional quando têm mais de $COUNT$ ligações +enabled=Ativado diff --git a/lang/app/strings/translations_ru.properties b/lang/app/strings/translations_ru.properties index 06c9f19e..32b3fc25 100644 --- a/lang/app/strings/translations_ru.properties +++ b/lang/app/strings/translations_ru.properties @@ -24,6 +24,7 @@ moveTo=Перейти к ... addDatabase=База данных ... browseInternalStorage=Просмотр внутреннего хранилища addTunnel=Туннель ... +addService=Сервис ... addScript=Скрипт ... addHost=Удаленный хост ... addShell=Shell Environment ... @@ -441,3 +442,20 @@ addNotes=Добавляй заметки order=Заказать ... stickToTop=Держись на высоте orderAheadOf=Заказать заранее ... +httpServer=HTTP-сервер +httpServerConfiguration=Конфигурация HTTP-сервера +httpServerPort=Порт +httpServerPortDescription=Порт, на котором будет прослушиваться HTTP-сервер.\n\nОбрати внимание, что если ты изменишь этот параметр, то все остальные приложения, которые взаимодействуют с сервером, тоже должны быть настроены на использование нового порта.\n\nТребуется перезагрузка для применения. +apiKey=Ключ API +apiKeyDescription=API-ключ для аутентификации API-запросов демона XPipe. Подробнее о том, как проходить аутентификацию, читай в общей документации по API.\n\nТребуется перезагрузка для применения. +disableApiAuthentication=Отключить аутентификацию API +disableApiAuthenticationDescription=Отключает все необходимые методы аутентификации, так что любой неаутентифицированный запрос будет обработан.\n\nАутентификацию следует отключать только в целях разработки.\n\nТребуется перезагрузка для применения. +api=API +storeIntroImportDescription=Уже используешь XPipe на другой системе? Синхронизируй существующие соединения на нескольких системах через удаленный git-репозиторий. Ты также можешь синхронизировать позже в любой момент, если он еще не настроен. +importConnections=Синхронизация соединений +importConnectionsTitle=Импортные соединения +showAllChildren=Показать всех детей +httpApi=HTTP API +isOnlySupportedLimit=поддерживается только профессиональной лицензией при наличии более $COUNT$ соединений +areOnlySupportedLimit=поддерживаются только профессиональной лицензией при наличии более чем $COUNT$ соединений +enabled=Включено diff --git a/lang/app/strings/translations_tr.properties b/lang/app/strings/translations_tr.properties index 24e7ed2b..48afcd66 100644 --- a/lang/app/strings/translations_tr.properties +++ b/lang/app/strings/translations_tr.properties @@ -24,6 +24,7 @@ moveTo=Taşınmak ... addDatabase=Veritabanı ... browseInternalStorage=Dahili depolama alanına göz atın addTunnel=Tünel ... +addService=Hizmet ... addScript=Senaryo ... addHost=Uzak Ana Bilgisayar ... addShell=Shell Çevre ... @@ -442,3 +443,20 @@ addNotes=Notlar ekleyin order=Sipariş ... stickToTop=Zirvede kal orderAheadOf=Önceden sipariş verin ... +httpServer=HTTP sunucusu +httpServerConfiguration=HTTP sunucu yapılandırması +httpServerPort=Liman +httpServerPortDescription=HTTP sunucusunun dinleyeceği bağlantı noktası.\n\nBunu değiştirirseniz, sunucuyla etkileşime giren diğer uygulamaların da yeni bağlantı noktasını kullanacak şekilde yapılandırılması gerektiğini unutmayın.\n\nUygulamak için yeniden başlatma gerekir. +apiKey=API anahtarı +apiKeyDescription=XPipe daemon API isteklerinin kimliğini doğrulamak için API anahtarı. Kimlik doğrulamanın nasıl yapılacağı hakkında daha fazla bilgi için genel API belgelerine bakın.\n\nUygulamak için yeniden başlatma gerekir. +disableApiAuthentication=API kimlik doğrulamasını devre dışı bırakma +disableApiAuthenticationDescription=Gerekli tüm kimlik doğrulama yöntemlerini devre dışı bırakır, böylece kimliği doğrulanmamış herhangi bir istek işlenir.\n\nKimlik doğrulama yalnızca geliştirme amacıyla devre dışı bırakılmalıdır.\n\nUygulamak için yeniden başlatma gerekir. +api=API +storeIntroImportDescription=XPipe'ı zaten başka bir sistemde mi kullanıyorsunuz? Mevcut bağlantılarınızı uzak bir git deposu aracılığıyla birden fazla sistem arasında senkronize edin. Henüz kurulmamışsa daha sonra istediğiniz zaman senkronize edebilirsiniz. +importConnections=Senkronizasyon bağlantıları +importConnectionsTitle=Bağlantıları İçe Aktar +showAllChildren=Tüm çocukları göster +httpApi=HTTP API +isOnlySupportedLimit=yalnızca $COUNT$ adresinden daha fazla bağlantıya sahip olunduğunda profesyonel lisans ile desteklenir +areOnlySupportedLimit=yalnızca $COUNT$ adresinden daha fazla bağlantıya sahip olunduğunda profesyonel lisans ile desteklenir +enabled=Etkin diff --git a/lang/app/strings/translations_zh.properties b/lang/app/strings/translations_zh.properties index 56d1dcc5..3f230851 100644 --- a/lang/app/strings/translations_zh.properties +++ b/lang/app/strings/translations_zh.properties @@ -24,6 +24,7 @@ moveTo=移动到 ... addDatabase=数据库 ... browseInternalStorage=浏览内部存储 addTunnel=隧道 ... +addService=服务 ... addScript=脚本 ... addHost=远程主机 ... addShell=外壳环境 ... @@ -441,3 +442,20 @@ addNotes=添加注释 order=订购 ... stickToTop=保持在顶部 orderAheadOf=提前订购... +httpServer=HTTP 服务器 +httpServerConfiguration=HTTP 服务器配置 +httpServerPort=端口 +httpServerPortDescription=HTTP 服务器监听的端口。\n\n请注意,如果更改端口,则与服务器交互的任何其他应用程序也需要配置为使用新端口。\n\n需要重新启动才能应用。 +apiKey=应用程序接口密钥 +apiKeyDescription=用于验证 XPipe 守护进程 API 请求的 API 密钥。有关如何验证的更多信息,请参阅一般 API 文档。\n\n需要重新启动才能应用。 +disableApiAuthentication=禁用 API 身份验证 +disableApiAuthenticationDescription=禁用所有必要的身份验证方法,以便处理任何未经身份验证的请求。\n\n只有出于开发目的才可禁用身份验证。\n\n需要重新启动才能应用。 +api=应用程序接口 +storeIntroImportDescription=已经在其他系统上使用 XPipe?通过远程 git 仓库在多个系统间同步您的现有连接。如果尚未设置,您也可以稍后随时同步。 +importConnections=同步连接 +importConnectionsTitle=导入连接 +showAllChildren=显示所有儿童 +httpApi=HTTP API +isOnlySupportedLimit=只有在连接数超过$COUNT$ 时才支持专业许可证 +areOnlySupportedLimit=只有在连接数超过$COUNT$ 时才支持专业许可证 +enabled=已启用 diff --git a/lang/base/strings/translations_da.properties b/lang/base/strings/translations_da.properties index 2cc61bc9..5a1b4711 100644 --- a/lang/base/strings/translations_da.properties +++ b/lang/base/strings/translations_da.properties @@ -66,7 +66,7 @@ isDefault=Køres på init i alle kompatible shells bringToShells=Bring til alle kompatible shells isDefaultGroup=Kør alle gruppescripts på shell init executionType=Udførelsestype -executionTypeDescription=Hvornår skal dette uddrag køres? +executionTypeDescription=I hvilke sammenhænge kan man bruge dette script minimumShellDialect=Shell-type minimumShellDialectDescription=Den påkrævede shell-type for dette script dumbOnly=Dum @@ -143,3 +143,23 @@ desktopCommand.displayName=Desktop-kommando desktopCommand.displayDescription=Kør en kommando i et fjernskrivebordsmiljø desktopCommandScript=Kommandoer desktopCommandScriptDescription=De kommandoer, der skal køres i miljøet +service.displayName=Service +service.displayDescription=Videresend en fjernservice til din lokale maskine +serviceLocalPort=Eksplicit lokal port +serviceLocalPortDescription=Den lokale port, der skal videresendes til, ellers bruges en tilfældig port +serviceRemotePort=Ekstern port +serviceRemotePortDescription=Den port, som tjenesten kører på +serviceHost=Servicevært +serviceHostDescription=Den vært, som tjenesten kører på +openWebsite=Åben hjemmeside +serviceGroup.displayName=Service-gruppe +serviceGroup.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 +runScript=Kør script ... +copyUrl=Kopier URL +fixedServiceGroup.displayName=Service-gruppe +fixedServiceGroup.displayDescription=Liste over tilgængelige tjenester på et system +mappedService.displayName=Service +mappedService.displayDescription=Interagere med en tjeneste, der er eksponeret af en container diff --git a/lang/base/strings/translations_de.properties b/lang/base/strings/translations_de.properties index 720b5384..e1a4873e 100644 --- a/lang/base/strings/translations_de.properties +++ b/lang/base/strings/translations_de.properties @@ -61,7 +61,7 @@ isDefault=Wird in allen kompatiblen Shells auf init ausgeführt bringToShells=Zu allen kompatiblen Shells bringen isDefaultGroup=Alle Gruppenskripte auf der Shell init ausführen executionType=Ausführungsart -executionTypeDescription=Wann dieses Snippet ausgeführt werden soll +executionTypeDescription=In welchen Kontexten ist dieses Skript zu verwenden? minimumShellDialect=Shell-Typ minimumShellDialectDescription=Der erforderliche Shell-Typ für dieses Skript dumbOnly=Dumm @@ -134,3 +134,23 @@ desktopCommand.displayName=Desktop-Befehl desktopCommand.displayDescription=Einen Befehl in einer Remote-Desktop-Umgebung ausführen desktopCommandScript=Befehle desktopCommandScriptDescription=Die Befehle, die in der Umgebung ausgeführt werden sollen +service.displayName=Dienst +service.displayDescription=Einen Ferndienst an deinen lokalen Rechner weiterleiten +serviceLocalPort=Expliziter lokaler Port +serviceLocalPortDescription=Der lokale Port, an den weitergeleitet werden soll, andernfalls wird ein zufälliger Port verwendet +serviceRemotePort=Entfernter Anschluss +serviceRemotePortDescription=Der Port, auf dem der Dienst läuft +serviceHost=Diensthost +serviceHostDescription=Der Host, auf dem der Dienst läuft +openWebsite=Website öffnen +serviceGroup.displayName=Dienstgruppe +serviceGroup.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 +runScript=Skript ausführen ... +copyUrl=URL kopieren +fixedServiceGroup.displayName=Dienstgruppe +fixedServiceGroup.displayDescription=Liste der verfügbaren Dienste auf einem System +mappedService.displayName=Dienst +mappedService.displayDescription=Interaktion mit einem Dienst, der von einem Container angeboten wird diff --git a/lang/base/strings/translations_en.properties b/lang/base/strings/translations_en.properties index 5f12de7a..f56a9e92 100644 --- a/lang/base/strings/translations_en.properties +++ b/lang/base/strings/translations_en.properties @@ -60,7 +60,7 @@ isDefault=Run on init in all compatible shells bringToShells=Bring to all compatible shells isDefaultGroup=Run all group scripts on shell init executionType=Execution type -executionTypeDescription=When to run this snippet +executionTypeDescription=In what contexts to use this script minimumShellDialect=Shell type minimumShellDialectDescription=The required shell type for this script dumbOnly=Dumb @@ -132,5 +132,25 @@ desktopCommand.displayName=Desktop command desktopCommand.displayDescription=Run a command in a remote desktop environment desktopCommandScript=Commands desktopCommandScriptDescription=The commands to run in the environment +service.displayName=Service +service.displayDescription=Forward a remote service to your local machine +serviceLocalPort=Explicit local port +serviceLocalPortDescription=The local port to forward to, otherwise a random one is used +serviceRemotePort=Remote port +serviceRemotePortDescription=The port on which the service is running on +serviceHost=Service host +serviceHostDescription=The host the service is running on +openWebsite=Open website +serviceGroup.displayName=Service group +serviceGroup.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 +runScript=Run script with +copyUrl=Copy URL +fixedServiceGroup.displayName=Service group +fixedServiceGroup.displayDescription=List the available services on a system +mappedService.displayName=Service +mappedService.displayDescription=Interact with a service exposed by a container diff --git a/lang/base/strings/translations_es.properties b/lang/base/strings/translations_es.properties index 758da880..6f01000e 100644 --- a/lang/base/strings/translations_es.properties +++ b/lang/base/strings/translations_es.properties @@ -60,7 +60,7 @@ isDefault=Se ejecuta en init en todos los shells compatibles bringToShells=Lleva a todas las conchas compatibles isDefaultGroup=Ejecutar todos los scripts de grupo en shell init executionType=Tipo de ejecución -executionTypeDescription=Cuándo ejecutar este fragmento +executionTypeDescription=En qué contextos utilizar este script minimumShellDialect=Tipo de carcasa minimumShellDialectDescription=El tipo de shell requerido para este script dumbOnly=Mudo @@ -132,3 +132,23 @@ desktopCommand.displayName=Comando de escritorio desktopCommand.displayDescription=Ejecutar un comando en un entorno de escritorio remoto desktopCommandScript=Comandos desktopCommandScriptDescription=Los comandos a ejecutar en el entorno +service.displayName=Servicio +service.displayDescription=Reenviar un servicio remoto a tu máquina local +serviceLocalPort=Puerto local explícito +serviceLocalPortDescription=El puerto local al que reenviar, de lo contrario se utiliza uno aleatorio +serviceRemotePort=Puerto remoto +serviceRemotePortDescription=El puerto en el que se ejecuta el servicio +serviceHost=Host de servicio +serviceHostDescription=El host en el que se ejecuta el servicio +openWebsite=Abrir sitio web +serviceGroup.displayName=Grupo de servicios +serviceGroup.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 +runScript=Ejecutar script ... +copyUrl=Copiar URL +fixedServiceGroup.displayName=Grupo de servicios +fixedServiceGroup.displayDescription=Enumerar los servicios disponibles en un sistema +mappedService.displayName=Servicio +mappedService.displayDescription=Interactúa con un servicio expuesto por un contenedor diff --git a/lang/base/strings/translations_fr.properties b/lang/base/strings/translations_fr.properties index 32350f2f..bcbde2a1 100644 --- a/lang/base/strings/translations_fr.properties +++ b/lang/base/strings/translations_fr.properties @@ -60,7 +60,7 @@ isDefault=S'exécute en mode init dans tous les shells compatibles bringToShells=Apporte à tous les coquillages compatibles isDefaultGroup=Exécute tous les scripts de groupe au démarrage de l'interpréteur de commandes executionType=Type d'exécution -executionTypeDescription=Quand exécuter cet extrait +executionTypeDescription=Dans quels contextes utiliser ce script minimumShellDialect=Type de coquille minimumShellDialectDescription=Le type d'interpréteur de commandes requis pour ce script dumbOnly=Muet @@ -132,3 +132,23 @@ desktopCommand.displayName=Commande de bureau desktopCommand.displayDescription=Exécuter une commande dans un environnement de bureau à distance desktopCommandScript=Commandes desktopCommandScriptDescription=Les commandes à exécuter dans l'environnement +service.displayName=Service +service.displayDescription=Transférer un service distant vers ta machine locale +serviceLocalPort=Port local explicite +serviceLocalPortDescription=Le port local vers lequel transférer, sinon un port aléatoire est utilisé +serviceRemotePort=Port distant +serviceRemotePortDescription=Le port sur lequel le service fonctionne +serviceHost=Hôte du service +serviceHostDescription=L'hôte sur lequel le service est exécuté +openWebsite=Ouvrir un site web +serviceGroup.displayName=Groupe de service +serviceGroup.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 +runScript=Exécute le script ... +copyUrl=Copier l'URL +fixedServiceGroup.displayName=Groupe de service +fixedServiceGroup.displayDescription=Liste les services disponibles sur un système +mappedService.displayName=Service +mappedService.displayDescription=Interagir avec un service exposé par un conteneur diff --git a/lang/base/strings/translations_it.properties b/lang/base/strings/translations_it.properties index ad162330..0d360788 100644 --- a/lang/base/strings/translations_it.properties +++ b/lang/base/strings/translations_it.properties @@ -60,7 +60,7 @@ isDefault=Eseguito su init in tutte le shell compatibili bringToShells=Porta a tutte le conchiglie compatibili isDefaultGroup=Esegui tutti gli script del gruppo all'avvio della shell executionType=Tipo di esecuzione -executionTypeDescription=Quando eseguire questo snippet +executionTypeDescription=In quali contesti utilizzare questo script minimumShellDialect=Tipo di shell minimumShellDialectDescription=Il tipo di shell richiesto per questo script dumbOnly=Muto @@ -132,3 +132,23 @@ desktopCommand.displayName=Comando sul desktop desktopCommand.displayDescription=Eseguire un comando in un ambiente desktop remoto desktopCommandScript=Comandi desktopCommandScriptDescription=I comandi da eseguire nell'ambiente +service.displayName=Servizio +service.displayDescription=Inoltrare un servizio remoto al computer locale +serviceLocalPort=Porta locale esplicita +serviceLocalPortDescription=La porta locale a cui inoltrare, altrimenti ne viene utilizzata una a caso +serviceRemotePort=Porta remota +serviceRemotePortDescription=La porta su cui è in esecuzione il servizio +serviceHost=Servizio host +serviceHostDescription=L'host su cui è in esecuzione il servizio +openWebsite=Sito web aperto +serviceGroup.displayName=Gruppo di servizio +serviceGroup.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 +runScript=Esegui script ... +copyUrl=Copia URL +fixedServiceGroup.displayName=Gruppo di servizio +fixedServiceGroup.displayDescription=Elenco dei servizi disponibili su un sistema +mappedService.displayName=Servizio +mappedService.displayDescription=Interagire con un servizio esposto da un contenitore diff --git a/lang/base/strings/translations_ja.properties b/lang/base/strings/translations_ja.properties index 07349e01..160be141 100644 --- a/lang/base/strings/translations_ja.properties +++ b/lang/base/strings/translations_ja.properties @@ -60,7 +60,7 @@ isDefault=すべての互換シェルでinit時に実行される bringToShells=すべての互換性のあるシェルに持ち込む isDefaultGroup=シェル init ですべてのグループスクリプトを実行する executionType=実行タイプ -executionTypeDescription=このスニペットを実行するタイミング +executionTypeDescription=このスクリプトをどのような文脈で使うか minimumShellDialect=シェルタイプ minimumShellDialectDescription=このスクリプトに必要なシェルタイプ dumbOnly=ダム @@ -132,3 +132,23 @@ desktopCommand.displayName=デスクトップコマンド desktopCommand.displayDescription=リモートデスクトップ環境でコマンドを実行する desktopCommandScript=コマンド desktopCommandScriptDescription=環境で実行するコマンド +service.displayName=サービス +service.displayDescription=リモートサービスをローカルマシンに転送する +serviceLocalPort=明示的なローカルポート +serviceLocalPortDescription=転送先のローカルポート。そうでない場合はランダムなポートが使われる。 +serviceRemotePort=リモートポート +serviceRemotePortDescription=サービスが実行されているポート +serviceHost=サービスホスト +serviceHostDescription=サービスが稼働しているホスト +openWebsite=オープンウェブサイト +serviceGroup.displayName=サービスグループ +serviceGroup.displayDescription=複数のサービスを1つのカテゴリーにまとめる +initScript=シェル init で実行する +shellScript=シェルセッション中にスクリプトを利用可能にする +fileScript=ファイルブラウザでファイル引数を指定してスクリプトを呼び出せるようにする +runScript=スクリプトを実行する +copyUrl=URLをコピーする +fixedServiceGroup.displayName=サービスグループ +fixedServiceGroup.displayDescription=システムで利用可能なサービスをリストアップする +mappedService.displayName=サービス +mappedService.displayDescription=コンテナによって公開されたサービスとやりとりする diff --git a/lang/base/strings/translations_nl.properties b/lang/base/strings/translations_nl.properties index 989c886d..e54d9114 100644 --- a/lang/base/strings/translations_nl.properties +++ b/lang/base/strings/translations_nl.properties @@ -60,7 +60,7 @@ isDefault=Uitvoeren op init in alle compatibele shells bringToShells=Breng naar alle compatibele shells isDefaultGroup=Alle groepsscripts uitvoeren op shell init executionType=Type uitvoering -executionTypeDescription=Wanneer dit knipsel uitvoeren +executionTypeDescription=In welke contexten kun je dit script gebruiken minimumShellDialect=Type omhulsel minimumShellDialectDescription=Het vereiste shelltype voor dit script dumbOnly=Stom @@ -132,3 +132,23 @@ desktopCommand.displayName=Desktop opdracht desktopCommand.displayDescription=Een opdracht uitvoeren in een externe desktopomgeving desktopCommandScript=Opdrachten desktopCommandScriptDescription=De commando's om uit te voeren in de omgeving +service.displayName=Service +service.displayDescription=Een service op afstand doorsturen naar je lokale machine +serviceLocalPort=Expliciete lokale poort +serviceLocalPortDescription=De lokale poort om naar door te sturen, anders wordt een willekeurige poort gebruikt +serviceRemotePort=Externe poort +serviceRemotePortDescription=De poort waarop de service draait +serviceHost=Service host +serviceHostDescription=De host waarop de service draait +openWebsite=Open website +serviceGroup.displayName=Servicegroep +serviceGroup.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 +runScript=Script uitvoeren ... +copyUrl=URL kopiëren +fixedServiceGroup.displayName=Servicegroep +fixedServiceGroup.displayDescription=Een lijst van beschikbare services op een systeem +mappedService.displayName=Service +mappedService.displayDescription=Interactie met een service die wordt aangeboden door een container diff --git a/lang/base/strings/translations_pt.properties b/lang/base/strings/translations_pt.properties index e22f39af..8d115e42 100644 --- a/lang/base/strings/translations_pt.properties +++ b/lang/base/strings/translations_pt.properties @@ -60,7 +60,7 @@ isDefault=Corre no init em todos os shells compatíveis bringToShells=Traz para todos os shells compatíveis isDefaultGroup=Executa todos os scripts de grupo no shell init executionType=Tipo de execução -executionTypeDescription=Quando deves executar este snippet +executionTypeDescription=Em que contextos podes utilizar este script minimumShellDialect=Tipo de shell minimumShellDialectDescription=O tipo de shell necessário para este script dumbOnly=Estúpido @@ -132,3 +132,23 @@ desktopCommand.displayName=Comando do ambiente de trabalho desktopCommand.displayDescription=Executa um comando num ambiente de trabalho remoto desktopCommandScript=Comandos desktopCommandScriptDescription=Os comandos a executar no ambiente +service.displayName=Serviço +service.displayDescription=Encaminhar um serviço remoto para a tua máquina local +serviceLocalPort=Porta local explícita +serviceLocalPortDescription=A porta local para a qual reencaminhar, caso contrário é utilizada uma porta aleatória +serviceRemotePort=Porta remota +serviceRemotePortDescription=A porta em que o serviço está a ser executado +serviceHost=Anfitrião de serviço +serviceHostDescription=O anfitrião em que o serviço está a ser executado +openWebsite=Abre o sítio Web +serviceGroup.displayName=Grupo de serviços +serviceGroup.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 +runScript=Executa o script ... +copyUrl=Copia o URL +fixedServiceGroup.displayName=Grupo de serviços +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 diff --git a/lang/base/strings/translations_ru.properties b/lang/base/strings/translations_ru.properties index 6f77f254..3640e64f 100644 --- a/lang/base/strings/translations_ru.properties +++ b/lang/base/strings/translations_ru.properties @@ -60,7 +60,7 @@ isDefault=Запускается в init во всех совместимых о bringToShells=Принесите всем совместимым оболочкам isDefaultGroup=Запустите все групповые скрипты на shell init executionType=Тип исполнения -executionTypeDescription=Когда запускать этот сниппет +executionTypeDescription=В каких контекстах использовать этот скрипт minimumShellDialect=Тип оболочки minimumShellDialectDescription=Необходимый тип оболочки для этого скрипта dumbOnly=Тупой @@ -132,3 +132,23 @@ desktopCommand.displayName=Команда рабочего стола desktopCommand.displayDescription=Выполнить команду в среде удаленного рабочего стола desktopCommandScript=Команды desktopCommandScriptDescription=Команды для запуска в среде +service.displayName=Сервис +service.displayDescription=Перенаправить удаленный сервис на локальную машину +serviceLocalPort=Явный локальный порт +serviceLocalPortDescription=Локальный порт для переадресации, в противном случае используется случайный порт +serviceRemotePort=Удаленный порт +serviceRemotePortDescription=Порт, на котором работает служба +serviceHost=Сервисный хост +serviceHostDescription=Хост, на котором запущена служба +openWebsite=Открытый сайт +serviceGroup.displayName=Группа услуг +serviceGroup.displayDescription=Сгруппируйте несколько сервисов в одну категорию +initScript=Запуск на shell init +shellScript=Сделать скрипт доступным во время сеанса оболочки +fileScript=Разрешить вызов скрипта с аргументами в виде файлов в браузере файлов +runScript=Запустите скрипт ... +copyUrl=Копировать URL +fixedServiceGroup.displayName=Группа услуг +fixedServiceGroup.displayDescription=Список доступных сервисов в системе +mappedService.displayName=Сервис +mappedService.displayDescription=Взаимодействие с сервисом, открываемым контейнером diff --git a/lang/base/strings/translations_tr.properties b/lang/base/strings/translations_tr.properties index 4bfc1484..3746f7bd 100644 --- a/lang/base/strings/translations_tr.properties +++ b/lang/base/strings/translations_tr.properties @@ -60,7 +60,7 @@ isDefault=Tüm uyumlu kabuklarda init üzerinde çalıştırın bringToShells=Tüm uyumlu kabukları getirin isDefaultGroup=Tüm grup komut dosyalarını kabuk başlangıcında çalıştırın executionType=Yürütme türü -executionTypeDescription=Bu snippet ne zaman çalıştırılır +executionTypeDescription=Bu komut dosyası hangi bağlamlarda kullanılmalı minimumShellDialect=Kabuk tipi minimumShellDialectDescription=Bu betik için gerekli kabuk türü dumbOnly=Aptal @@ -132,3 +132,23 @@ desktopCommand.displayName=Masaüstü komutu desktopCommand.displayDescription=Uzak masaüstü ortamında bir komut çalıştırma desktopCommandScript=Komutlar desktopCommandScriptDescription=Ortamda çalıştırılacak komutlar +service.displayName=Hizmet +service.displayDescription=Uzak bir hizmeti yerel makinenize iletme +serviceLocalPort=Açık yerel bağlantı noktası +serviceLocalPortDescription=Yönlendirilecek yerel bağlantı noktası, aksi takdirde rastgele bir bağlantı noktası kullanılır +serviceRemotePort=Uzak bağlantı noktası +serviceRemotePortDescription=Hizmetin üzerinde çalıştığı bağlantı noktası +serviceHost=Hizmet sunucusu +serviceHostDescription=Hizmetin üzerinde çalıştığı ana bilgisayar +openWebsite=Açık web sitesi +serviceGroup.displayName=Hizmet grubu +serviceGroup.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 +runScript=Komut dosyasını çalıştır ... +copyUrl=URL'yi kopyala +fixedServiceGroup.displayName=Hizmet grubu +fixedServiceGroup.displayDescription=Bir sistemdeki mevcut hizmetleri listeleme +mappedService.displayName=Hizmet +mappedService.displayDescription=Bir konteyner tarafından sunulan bir hizmetle etkileşim diff --git a/lang/base/strings/translations_zh.properties b/lang/base/strings/translations_zh.properties index 5cb6b6e4..ce985318 100644 --- a/lang/base/strings/translations_zh.properties +++ b/lang/base/strings/translations_zh.properties @@ -60,7 +60,7 @@ isDefault=在所有兼容外壳的 init 中运行 bringToShells=带入所有兼容外壳 isDefaultGroup=在 shell 启动时运行所有组脚本 executionType=执行类型 -executionTypeDescription=何时运行此片段 +executionTypeDescription=在哪些情况下使用此脚本 minimumShellDialect=外壳类型 minimumShellDialectDescription=该脚本所需的 shell 类型 dumbOnly=笨 @@ -132,3 +132,23 @@ desktopCommand.displayName=桌面命令 desktopCommand.displayDescription=在远程桌面环境中运行命令 desktopCommandScript=命令 desktopCommandScriptDescription=在环境中运行的命令 +service.displayName=服务 +service.displayDescription=将远程服务转发到本地计算机 +serviceLocalPort=显式本地端口 +serviceLocalPortDescription=要转发到的本地端口,否则使用随机端口 +serviceRemotePort=远程端口 +serviceRemotePortDescription=服务运行的端口 +serviceHost=服务主机 +serviceHostDescription=服务运行的主机 +openWebsite=打开网站 +serviceGroup.displayName=服务组 +serviceGroup.displayDescription=将多项服务归为一类 +initScript=在 shell init 上运行 +shellScript=在 shell 会话中提供脚本 +fileScript=允许在文件浏览器中使用文件参数调用脚本 +runScript=运行脚本 ... +copyUrl=复制 URL +fixedServiceGroup.displayName=服务组 +fixedServiceGroup.displayDescription=列出系统中可用的服务 +mappedService.displayName=服务 +mappedService.displayDescription=与容器暴露的服务交互 diff --git a/lang/base/texts/executionType_da.md b/lang/base/texts/executionType_da.md index db58848e..a9617f68 100644 --- a/lang/base/texts/executionType_da.md +++ b/lang/base/texts/executionType_da.md @@ -1,15 +1,50 @@ -## Udførelsestyper +# Udførelsestyper -Der er to forskellige eksekveringstyper, når XPipe opretter forbindelse til et system. +Du kan bruge et script i flere forskellige scenarier. -### I baggrunden +Når du aktiverer et script, dikterer udførelsestyperne, hvad XPipe vil gøre med scriptet. -Den første forbindelse til et system oprettes i baggrunden i en dumb terminal-session. +## Init-scripts -Blokeringskommandoer, der kræver brugerinput, kan fryse shell-processen, når XPipe starter den op internt i baggrunden. For at undgå dette bør du kun kalde disse blokerende kommandoer i terminaltilstand. +Når et script er angivet som init-script, kan det vælges i shell-miljøer. -Filbrowseren bruger for eksempel udelukkende den dumme baggrundstilstand til at håndtere sine operationer, så hvis du vil have dit scriptmiljø til at gælde for filbrowsersessionen, skal det køre i den dumme tilstand. +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 +``` +alias ll="ls -l" +alias la="ls -A" +alias l="ls -CF" +``` +du vil have adgang til disse aliasser i alle kompatible shell-sessioner, hvis scriptet er aktiveret. + +## Shell-scripts + +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. +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 +``` +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. + +## 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. -### I terminalerne -Når den indledende dumb terminal-forbindelse er lykkedes, vil XPipe åbne en separat forbindelse i den faktiske terminal. Hvis du vil have scriptet til at køre, når du åbner forbindelsen i en terminal, skal du vælge terminaltilstand. diff --git a/lang/base/texts/executionType_de.md b/lang/base/texts/executionType_de.md index 45b36bcf..cc16f49e 100644 --- a/lang/base/texts/executionType_de.md +++ b/lang/base/texts/executionType_de.md @@ -1,15 +1,50 @@ -## Ausführungsarten +# Ausführungsarten -Es gibt zwei verschiedene Ausführungsarten, wenn XPipe eine Verbindung zu einem System herstellt. +Du kannst ein Skript in vielen verschiedenen Szenarien verwenden. -### Im Hintergrund +Wenn du ein Skript aktivierst, legen die Ausführungsarten fest, was XPipe mit dem Skript tun soll. -Die erste Verbindung zu einem System wird im Hintergrund in einer stummen Terminalsitzung hergestellt. +## Init-Skripte -Blockierende Befehle, die Benutzereingaben erfordern, können den Shell-Prozess einfrieren, wenn XPipe ihn intern zuerst im Hintergrund startet. Um dies zu vermeiden, solltest du diese blockierenden Befehle nur im Terminalmodus aufrufen. +Wenn ein Skript als Init-Skript gekennzeichnet ist, kann es in Shell-Umgebungen ausgewählt werden. -Der Dateibrowser z. B. verwendet für seine Operationen ausschließlich den dummen Hintergrundmodus. Wenn du also möchtest, dass deine Skriptumgebung für die Dateibrowser-Sitzung gilt, sollte sie im dummen Modus ausgeführt werden. +Wenn ein Skript aktiviert ist, wird es außerdem automatisch bei init in allen kompatiblen Shells ausgeführt. + +Wenn du zum Beispiel ein einfaches Init-Skript erstellst wie +``` +alias ll="ls -l" +alias la="ls -A" +alias l="ls -CF" +``` +hast du in allen kompatiblen Shell-Sitzungen Zugang zu diesen Aliasen, wenn das Skript aktiviert ist. + +## Shell-Skripte + +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. + +Wenn du zum Beispiel ein einfaches Shell-Skript mit dem Namen `apti` wie folgt erstellst +``` +sudo apt install "$1" +``` +kannst du das auf jedem kompatiblen System mit `apti.sh ` aufrufen, wenn das Skript aktiviert ist. + +## Datei-Skripte + +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 +``` +sudo apt install "$@" +``` +kannst du das Skript für ausgewählte Dateien ausführen, 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. -### In den Terminals -Nachdem die anfängliche Dumb-Terminal-Verbindung erfolgreich war, öffnet XPipe eine separate Verbindung im eigentlichen Terminal. Wenn du möchtest, dass das Skript ausgeführt wird, wenn du die Verbindung in einem Terminal öffnest, dann wähle den Terminalmodus. diff --git a/lang/base/texts/executionType_en.md b/lang/base/texts/executionType_en.md index 68e77d4b..7a0dbec4 100644 --- a/lang/base/texts/executionType_en.md +++ b/lang/base/texts/executionType_en.md @@ -1,15 +1,50 @@ -## Execution types +# Execution types -There are two distinct execution types when XPipe connects to a system. +You can use a script in multiple different scenarios. -### In the background +When enabling a script, the execution types dictate what XPipe will do with the script. -The first connection to a system is made in the background in a dumb terminal session. +## Init scripts -Blocking commands that require user input can freeze the shell process when XPipe starts it up internally first in the background. To avoid this, you should only call these blocking commands in the terminal mode. +When a script is designated as init script, it can be selected in shell environments. -The file browser for example entirely uses the dumb background mode to handle its operations, so if you want your script environment to apply to the file browser session, it should run in the dumb mode. +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 +``` +alias ll="ls -l" +alias la="ls -A" +alias l="ls -CF" +``` +you will have access to these aliases in all compatible shell sessions if the script is enabled. + +## Shell scripts + +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. + +For example, if you create a simple shell script named `apti` like +``` +sudo apt install "$1" +``` +you can call that on any compatible system with `apti.sh ` if the script is enabled. + +## File scripts + +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 +``` +sudo apt install "$@" +``` +you can run the script on selected files 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. -### In the terminals -After the initial dumb terminal connection has succeeded, XPipe will open a separate connection in the actual terminal. If you want the script to be run when you open the connection in a terminal, then choose the terminal mode. diff --git a/lang/base/texts/executionType_es.md b/lang/base/texts/executionType_es.md index 945fb99c..5cc3852c 100644 --- a/lang/base/texts/executionType_es.md +++ b/lang/base/texts/executionType_es.md @@ -1,15 +1,50 @@ -## Tipos de ejecución +# Tipos de ejecución -Hay dos tipos de ejecución distintos cuando XPipe se conecta a un sistema. +Puedes utilizar un script en múltiples escenarios diferentes. -### En segundo plano +Al activar un script, los tipos de ejecución dictan lo que XPipe hará con el script. -La primera conexión a un sistema se realiza en segundo plano en una sesión de terminal tonta. +## Guiones de inicio -Los comandos de bloqueo que requieren la entrada del usuario pueden congelar el proceso shell cuando XPipe lo inicia internamente por primera vez en segundo plano. Para evitarlo, sólo debes llamar a estos comandos de bloqueo en el modo terminal. +Cuando un script se designa como script init, se puede seleccionar en entornos shell. -El explorador de archivos, por ejemplo, utiliza enteramente el modo mudo en segundo plano para manejar sus operaciones, así que si quieres que tu entorno de script se aplique a la sesión del explorador de archivos, debe ejecutarse en el modo mudo. +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 +``` +alias ll="ls -l" +alias la="ls -A" +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 + +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. +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 +``` +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. + +## 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. -### En los terminales -Después de que la conexión inicial de terminal mudo haya tenido éxito, XPipe abrirá una conexión separada en el terminal real. Si quieres que el script se ejecute al abrir la conexión en un terminal, elige el modo terminal. diff --git a/lang/base/texts/executionType_fr.md b/lang/base/texts/executionType_fr.md index 0a502bb3..63d1f1b9 100644 --- a/lang/base/texts/executionType_fr.md +++ b/lang/base/texts/executionType_fr.md @@ -1,15 +1,50 @@ -## Types d'exécution +# Types d'exécution -Il existe deux types d'exécution distincts lorsque XPipe se connecte à un système. +Tu peux utiliser un script dans plusieurs scénarios différents. -### En arrière-plan +Lors de l'activation d'un script, les types d'exécution dictent ce que XPipe fera avec le script. -La première connexion à un système est effectuée en arrière-plan dans une session de terminal muet. +## Init scripts -Les commandes de blocage qui nécessitent une entrée de la part de l'utilisateur peuvent geler le processus de l'interpréteur de commandes lorsque XPipe le démarre d'abord en interne en arrière-plan. Pour éviter cela, tu ne dois appeler ces commandes bloquantes qu'en mode terminal. +Lorsqu'un script est désigné comme script init, il peut être sélectionné dans les environnements shell. -Le navigateur de fichiers, par exemple, utilise entièrement le mode d'arrière-plan muet pour gérer ses opérations, donc si tu veux que l'environnement de ton script s'applique à la session du navigateur de fichiers, il doit s'exécuter en mode muet. +De plus, si un script est activé, il sera automatiquement exécuté lors de l'init dans tous les shells compatibles. + +Par exemple, si tu crées un script init simple comme +``` +alias ll="ls -l" +alias la="ls -A" +alias l="ls -CF" +``` +tu auras accès à ces alias dans toutes les sessions shell compatibles si le script est activé. + +## Scripts shell + +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. + +Par exemple, si tu crées un script shell simple nommé `apti` comme suit +``` +sudo apt install "$1" +``` +vous pouvez l'appeler sur n'importe quel système compatible avec `apti.sh ` si le script est activé. + +## Fichier scripts + +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 +``` +sudo apt install "$@" +``` +tu peux exécuter le script sur les fichiers sélectionnés 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. -### Dans les terminaux -Une fois que la connexion initiale au terminal muet a réussi, XPipe ouvre une connexion séparée dans le terminal réel. Si tu veux que le script soit exécuté lorsque tu ouvres la connexion dans un terminal, choisis le mode terminal. diff --git a/lang/base/texts/executionType_it.md b/lang/base/texts/executionType_it.md index 1254010e..d3c07a92 100644 --- a/lang/base/texts/executionType_it.md +++ b/lang/base/texts/executionType_it.md @@ -1,15 +1,50 @@ -## Tipi di esecuzione +# Tipi di esecuzione -Esistono due tipi di esecuzione distinti quando XPipe si connette a un sistema. +Puoi utilizzare uno script in diversi scenari. -### In background +Quando abiliti uno script, i tipi di esecuzione stabiliscono cosa XPipe farà con lo script. -La prima connessione a un sistema avviene in background in una sessione di terminale muta. +## Script di avvio -I comandi di blocco che richiedono l'input dell'utente possono bloccare il processo di shell quando XPipe lo avvia internamente in background. Per evitare questo problema, dovresti chiamare questi comandi di blocco solo in modalità terminale. +Quando uno script è designato come script di avvio, può essere selezionato negli ambienti shell. -Il navigatore di file, ad esempio, utilizza esclusivamente la modalità di sfondo muta per gestire le sue operazioni, quindi se vuoi che il tuo ambiente di script si applichi alla sessione del navigatore di file, deve essere eseguito in modalità muta. +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 +``` +alias ll="ls -l" +alias la="ls -A" +alias l="ls -CF" +``` +avrai accesso a questi alias in tutte le sessioni di shell compatibili se lo script è abilitato. + +## Script di shell + +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. +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 +``` +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. + +## 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. -### Nei terminali -Dopo che la connessione iniziale del terminale muto è riuscita, XPipe aprirà una connessione separata nel terminale vero e proprio. Se vuoi che lo script venga eseguito quando apri la connessione in un terminale, scegli la modalità terminale. diff --git a/lang/base/texts/executionType_ja.md b/lang/base/texts/executionType_ja.md index 4838ec54..56656bec 100644 --- a/lang/base/texts/executionType_ja.md +++ b/lang/base/texts/executionType_ja.md @@ -1,15 +1,50 @@ -## 実行タイプ +# 実行タイプ -XPipeがシステムに接続する際、2種類の実行タイプがある。 +スクリプトは複数の異なるシナリオで使用できる。 -### バックグラウンド +スクリプトを有効にする場合、実行タイプによってXPipeがスクリプトで何を行うかが決まる。 -システムへの最初の接続は、ダム端末セッションでバックグラウンドで行われる。 +## スクリプトの初期化 -ユーザー入力を必要とするブロックコマンドは、XPipeがバックグラウンドで最初にシェルプロセスを内部的に起動する際に、シェルプロセスをフリーズさせる可能性がある。これを避けるため、これらのブロックコマンドはターミナルモードでのみ呼び出すべきである。 +スクリプトをinitスクリプトとして指定すると、シェル環境で選択できるようになる。 -例えばファイルブラウザは完全にダムバックグラウンドモードを使用して操作を処理するため、スクリプト環境をファイルブラウザセッションに適用したい場合は、ダムモードで実行する必要がある。 +さらに、スクリプトが有効になっていれば、互換性のあるすべてのシェルで、init時に自動的に実行される。 + +例えば、次のような単純なinitスクリプトを作成した場合 +``` +alias ll="ls -l" +alias la="ls -A" +alias l="ls -CF" +``` +スクリプトが有効になっていれば、互換性のあるすべてのシェル・セッションでこれらのエイリアスにアクセスできる。 + +## シェルスクリプト + +通常のシェルスクリプトは、ターミナル上のシェルセッションで呼び出され ることを想定している。 +有効にすると、スクリプトはターゲットシステムにコピーされ、 すべての互換シェルでPATHに入れられる。 +これにより、ターミナル・セッションのどこからでもスクリプトを呼び出すことができる。 +スクリプト名は小文字になり、スペースはアンダースコアに置き換えられるので、簡単にスクリプトを呼び出すことができる。 + +例えば、次のような`apti`という単純なシェルスクリプトを作成した場合、次のようになる。 +``` +sudo apt install "$1" +``` +スクリプトが有効になっていれば、互換性のあるシステム上で`apti.sh `を使ってそれを呼び出すことができる。 + +## ファイルスクリプト + +最後に、ファイルブラウザのインターフェイスからファイル入力を使ってカスタムスクリプトを実行することもできる。 +ファイルスクリプトが有効になると、ファイルブラウザに表示され、ファイル入力で実行できるようになる。 + +例えば、次のような簡単なファイルスクリプトを作成した場合 +``` +sudo apt install "$@" +``` +スクリプトが有効になっていれば、選択したファイルに対してスクリプトを実行できる。 + +## 複数のタイプ + +ファイルスクリプトのサンプルは、上のシェルスクリプトのサンプルと同じである、 +スクリプトを複数のシナリオで使用する場合は、スクリプトの実行タイプに複数のチェックボックスを付けることもできる。 -### ターミナルでは -最初のダムターミナル接続が成功すると、XPipeは実際のターミナルで別の接続を開く。ターミナルで接続を開いたときにスクリプトを実行させたい場合は、ターミナルモードを選択する。 diff --git a/lang/base/texts/executionType_nl.md b/lang/base/texts/executionType_nl.md index 50538432..c471f03d 100644 --- a/lang/base/texts/executionType_nl.md +++ b/lang/base/texts/executionType_nl.md @@ -1,15 +1,50 @@ -## Uitvoeringstypes +# Uitvoeringstypen -Er zijn twee verschillende uitvoeringstypen wanneer XPipe verbinding maakt met een systeem. +Je kunt een script in meerdere verschillende scenario's gebruiken. -### Op de achtergrond +Wanneer je een script inschakelt, bepalen de uitvoeringstypen wat XPipe met het script zal doen. -De eerste verbinding met een systeem wordt op de achtergrond gemaakt in een domme terminal sessie. +## Init scripts -Blokkerende commando's die gebruikersinvoer vereisen kunnen het shell proces bevriezen wanneer XPipe het eerst intern op de achtergrond opstart. Om dit te voorkomen, moet je deze blokkerende commando's alleen in de terminalmodus aanroepen. +Als een script is aangewezen als init-script, kan het worden geselecteerd in shell-omgevingen. -De bestandsbrowser bijvoorbeeld gebruikt volledig de domme achtergrondmodus om zijn bewerkingen af te handelen, dus als je wilt dat je scriptomgeving van toepassing is op de bestandsbrowsersessie, moet deze in de domme modus draaien. +Bovendien, als een script is ingeschakeld, zal het automatisch worden uitgevoerd op init in alle compatibele shells. + +Als je bijvoorbeeld een eenvoudig init-script maakt als +``` +alias ll="ls -l" +alias la="ls -A" +alias l="ls -CF" +``` +je hebt toegang tot deze aliassen in alle compatibele shell sessies als het script is ingeschakeld. + +## Shell scripts + +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. + +Als je bijvoorbeeld een eenvoudig shellscript maakt met de naam `apti` zoals +``` +sudo apt install "$1" +``` +kun je dat op elk compatibel systeem aanroepen met `apti.sh ` als het script is ingeschakeld. + +## Bestandsscripts + +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 +``` +sudo apt install "$@" +``` +kun je het script uitvoeren op geselecteerde bestanden 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. -### In de terminals -Nadat de initiële domme terminalverbinding is gelukt, opent XPipe een aparte verbinding in de echte terminal. Als je wilt dat het script wordt uitgevoerd wanneer je de verbinding in een terminal opent, kies dan de terminalmodus. diff --git a/lang/base/texts/executionType_pt.md b/lang/base/texts/executionType_pt.md index a04bf515..fdd4c2fb 100644 --- a/lang/base/texts/executionType_pt.md +++ b/lang/base/texts/executionType_pt.md @@ -1,15 +1,50 @@ -## Tipos de execução +# Tipos de execução -Existem dois tipos de execução distintos quando o XPipe se liga a um sistema. +Podes utilizar um script em vários cenários diferentes. -### Em segundo plano +Ao ativar um script, os tipos de execução ditam o que o XPipe fará com o script. -A primeira conexão com um sistema é feita em segundo plano em uma sessão de terminal burro. +## Scripts de inicialização -Os comandos de bloqueio que requerem a entrada do usuário podem congelar o processo do shell quando o XPipe o inicia internamente primeiro em segundo plano. Para evitar isso, só deves chamar estes comandos de bloqueio no modo terminal. +Quando um script é designado como script de inicialização, ele pode ser selecionado em ambientes shell. -O navegador de ficheiros, por exemplo, utiliza inteiramente o modo de fundo burro para tratar das suas operações, por isso, se quiseres que o teu ambiente de script se aplique à sessão do navegador de ficheiros, deve ser executado no modo burro. +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 +``` +alias ll="ls -l" +alias la="ls -A" +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 + +Um script de shell normal 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 +``` +sudo apt install "$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. -### Nos terminais -Depois que a conexão inicial do terminal burro for bem-sucedida, o XPipe abrirá uma conexão separada no terminal real. Se quiseres que o script seja executado quando abrires a ligação num terminal, então escolhe o modo terminal. diff --git a/lang/base/texts/executionType_ru.md b/lang/base/texts/executionType_ru.md index f7ff4fb0..ab81514b 100644 --- a/lang/base/texts/executionType_ru.md +++ b/lang/base/texts/executionType_ru.md @@ -1,15 +1,50 @@ -## Типы исполнения +# Типы исполнения -Есть два разных типа исполнения, когда XPipe подключается к системе. +Ты можешь использовать скрипт в нескольких различных сценариях. -### В фоновом режиме +При включении скрипта типы выполнения определяют, что XPipe будет делать со скриптом. -Первое подключение к системе происходит в фоновом режиме в тупой терминальной сессии. +## Начальные скрипты -Блокирующие команды, требующие пользовательского ввода, могут заморозить процесс оболочки, когда XPipe запускает его сначала внутри системы в фоновом режиме. Чтобы этого избежать, вызывай эти блокирующие команды только в терминальном режиме. +Когда скрипт обозначен как init script, он может быть выбран в среде оболочки. -Например, файловый браузер полностью использует немой фоновый режим для обработки своих операций, поэтому, если ты хочешь, чтобы окружение твоего скрипта применялось к сессии файлового браузера, он должен запускаться в немом режиме. +Более того, если скрипт включен, он будет автоматически запускаться при init во всех совместимых оболочках. + +Например, если ты создашь простой init-скрипт типа +``` +alias ll="ls -l" +alias la="ls -A" +alias l="ls -CF" +``` +ты будешь иметь доступ к этим псевдонимам во всех совместимых сессиях оболочки, если скрипт включен. + +## Скрипты оболочки + +Обычный shell-скрипт предназначен для вызова в shell-сессии в твоем терминале. +При включении скрипта он будет скопирован в целевую систему и помещен в PATH во всех совместимых оболочках. +Это позволит тебе вызывать скрипт из любого места терминальной сессии. +Имя скрипта будет написано в нижнем регистре, а пробелы будут заменены на подчеркивания, что позволит тебе легко вызывать скрипт. + +Например, если ты создашь простой shell-скрипт с именем `apti`, например +``` +sudo apt install "$1" +``` +ты сможешь вызвать его на любой совместимой системе с помощью `apti.sh `, если скрипт включен. + +## Скрипты файлов + +Наконец, ты также можешь запускать пользовательские скрипты с файловыми входами из интерфейса файлового браузера. +Когда файловый скрипт включен, он будет отображаться в браузере файлов, чтобы его можно было запустить с файловыми входами. + +Например, если ты создашь простой файловый скрипт типа +``` +sudo apt install "$@" +``` +ты сможешь запускать скрипт на выбранных файлах, если он включен. + +## Несколько типов + +Поскольку пример файлового скрипта такой же, как и пример shell-скрипта выше, +ты видишь, что также можешь поставить несколько галочек напротив типов выполнения скрипта, если он должен использоваться в нескольких сценариях. -### В терминалах -После того как первоначальное подключение к тупому терминалу прошло успешно, XPipe откроет отдельное соединение в реальном терминале. Если ты хочешь, чтобы скрипт запускался при открытии соединения в терминале, то выбирай терминальный режим. diff --git a/lang/base/texts/executionType_tr.md b/lang/base/texts/executionType_tr.md index df61a9a8..0827d811 100644 --- a/lang/base/texts/executionType_tr.md +++ b/lang/base/texts/executionType_tr.md @@ -1,15 +1,50 @@ -## Yürütme türleri +# Yürütme türleri -XPipe bir sisteme bağlandığında iki farklı yürütme türü vardır. +Bir komut dosyasını birden fazla farklı senaryoda kullanabilirsiniz. -### Arka planda +Bir komut dosyası etkinleştirilirken, yürütme türleri XPipe'ın komut dosyasıyla ne yapacağını belirler. -Bir sisteme ilk bağlantı arka planda bir aptal terminal oturumunda yapılır. +## Başlangıç betikleri -Kullanıcı girişi gerektiren engelleme komutları, XPipe arka planda ilk olarak dahili olarak başlatıldığında kabuk sürecini dondurabilir. Bunu önlemek için, bu engelleme komutlarını yalnızca terminal modunda çağırmalısınız. +Bir komut dosyası init komut dosyası olarak belirlendiğinde, kabuk ortamlarında seçilebilir. -Örneğin dosya tarayıcısı, işlemlerini gerçekleştirmek için tamamen dilsiz arka plan modunu kullanır; bu nedenle, kod ortamınızın dosya tarayıcısı oturumuna uygulanmasını istiyorsanız, dilsiz modda çalışması gerekir. +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 +``` +alias ll="ls -l" +alias la="ls -A" +alias l="ls -CF" +``` +betik etkinleştirilmişse, tüm uyumlu kabuk oturumlarında bu takma adlara erişebileceksiniz. + +## Kabuk betikleri + +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. + +Örneğin, `apti` adında aşağıdaki gibi basit bir kabuk betiği oluşturursanız +``` +sudo apt install "$1" +``` +betik etkinleştirilmişse bunu uyumlu herhangi bir sistemde `apti.sh ` ile çağırabilirsiniz. + +## Dosya komut dosyaları + +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 +``` +sudo apt install "$@" +``` +komut dosyası etkinleştirilmişse komut dosyasını seçilen dosyalar üzerinde çalıştı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. -### Terminallerde -İlk dumb terminal bağlantısı başarılı olduktan sonra, XPipe gerçek terminalde ayrı bir bağlantı açacaktır. Bağlantıyı bir terminalde açtığınızda komut dosyasının çalıştırılmasını istiyorsanız, terminal modunu seçin. diff --git a/lang/base/texts/executionType_zh.md b/lang/base/texts/executionType_zh.md index a223fd8e..4e54998e 100644 --- a/lang/base/texts/executionType_zh.md +++ b/lang/base/texts/executionType_zh.md @@ -1,15 +1,50 @@ -## 执行类型 +# 执行类型 -XPipe 连接到系统时有两种不同的执行类型。 +您可以在多种不同情况下使用脚本。 -### 在后台 +启用脚本时,执行类型决定了 XPipe 将如何处理脚本。 -与系统的首次连接是在后台的哑终端会话中进行的。 +## 初始脚本 -当 XPipe 首先在后台内部启动 shell 进程时,需要用户输入的阻塞命令会冻结 shell 进程。为避免出现这种情况,只能在终端模式下调用这些阻塞命令。 +当脚本被指定为初始脚本时,它可以在 shell 环境中被选择。 -例如,文件浏览器完全使用哑模式后台处理其操作,因此如果您希望脚本环境适用于文件浏览器会话,则应在哑模式下运行。 +此外,如果脚本被启用,它将在所有兼容的 shell 中自动运行 init 脚本。 + +例如,如果创建一个简单的启动脚本,如 +``` +别名 ll="ls -l" +alias la="ls -A" +别名 l="ls -CF" +``` +如果脚本已启用,您就可以在所有兼容的 shell 会话中访问这些别名。 + +##hell 脚本 + +普通 shell 脚本用于在终端的 shell 会话中调用。 +启用后,脚本将被复制到目标系统,并放入所有兼容 shell 的 PATH 中。 +这样就可以在终端会话的任何地方调用脚本。 +脚本名称将小写,空格将用下划线代替,以便于调用脚本。 + +例如,如果创建一个名为 `apti` 的简单 shell 脚本,如 +``` +sudo apt install "$1" +``` +如果脚本已启用,你就可以在任何兼容系统上使用 `apti.sh ` 调用该脚本。 + +## 文件脚本 + +最后,你还可以通过文件浏览器界面的文件输入运行自定义脚本。 +启用文件脚本后,它将显示在文件浏览器中,可通过文件输入运行。 + +例如,如果你创建了一个简单的文件脚本,如 +``` +sudo apt install "$@" +``` +这样的简单文件脚本,如果脚本已启用,你就可以在选定的文件上运行该脚本。 + +## 多种类型 + +由于示例文件脚本与上述示例 shell 脚本相同、 +你可以看到,如果脚本应在多种情况下使用,你也可以为脚本的执行类型勾选多个复选框。 -### 在终端中 -初始哑终端连接成功后,XPipe 将在实际终端中打开一个单独的连接。如果您希望在终端打开连接时运行脚本,那么请选择终端模式。 diff --git a/lang/proc/strings/translations_da.properties b/lang/proc/strings/translations_da.properties index 66ca48e0..7dc24c6d 100644 --- a/lang/proc/strings/translations_da.properties +++ b/lang/proc/strings/translations_da.properties @@ -166,6 +166,7 @@ startContainer=Start container stopContainer=Stop container #custom inspectContainer=Inspicér container +inspectContext=Inspicér kontekst k8sClusterNameDescription=Navnet på den kontekst, klyngen befinder sig i. #custom pod=Pod @@ -347,3 +348,12 @@ rdpUsernameDescription=Til brugeren for at logge ind som addressDescription=Hvor skal man oprette forbindelse til rdpAdditionalOptions=Yderligere RDP-muligheder rdpAdditionalOptionsDescription=Rå RDP-muligheder, der skal inkluderes, formateret på samme måde som i .rdp-filer +proxmoxVncConfirmTitle=VNC-opsætning +proxmoxVncConfirmHeader=Vil du aktivere VNC for den virtuelle maskine? +proxmoxVncConfirmContent=Dette opsætter en tilgængelig VNC-server og genstarter den virtuelle maskine. Du skal så vente, indtil maskinen er startet op igen, før du opretter forbindelse. +dockerContext.displayName=Docker-kontekst +dockerContext.displayDescription=Interagerer med containere i en bestemt kontekst +containerActions=Container-handlinger +vmActions=VM-handlinger +dockerContextActions=Kontekst-handlinger +k8sPodActions=Pod-handlinger diff --git a/lang/proc/strings/translations_de.properties b/lang/proc/strings/translations_de.properties index 4ed3ef51..c5dcfed8 100644 --- a/lang/proc/strings/translations_de.properties +++ b/lang/proc/strings/translations_de.properties @@ -155,6 +155,7 @@ shells=Verfügbare Shells startContainer=Container starten stopContainer=Container anhalten inspectContainer=Container inspizieren +inspectContext=Kontext inspizieren k8sClusterNameDescription=Der Name des Kontexts, in dem sich der Cluster befindet. pod=Pod podName=Pod-Name @@ -325,3 +326,12 @@ rdpUsernameDescription=An Benutzer, der sich anmelden soll als addressDescription=Wohin soll die Verbindung gehen? rdpAdditionalOptions=Zusätzliche RDP-Optionen rdpAdditionalOptionsDescription=Rohe RDP-Optionen, die genauso formatiert sind wie in .rdp-Dateien +proxmoxVncConfirmTitle=VNC-Einrichtung +proxmoxVncConfirmHeader=Willst du VNC für die virtuelle Maschine aktivieren? +proxmoxVncConfirmContent=Dadurch wird ein zugänglicher VNC-Server eingerichtet und die virtuelle Maschine neu gestartet. Du solltest dann warten, bis die Maschine wieder hochgefahren ist, bevor du eine Verbindung herstellst. +dockerContext.displayName=Docker-Kontext +dockerContext.displayDescription=Interaktion mit Containern, die sich in einem bestimmten Kontext befinden +containerActions=Container-Aktionen +vmActions=VM-Aktionen +dockerContextActions=Kontextbezogene Aktionen +k8sPodActions=Pod-Aktionen diff --git a/lang/proc/strings/translations_en.properties b/lang/proc/strings/translations_en.properties index fe7e51c7..ea2b419a 100644 --- a/lang/proc/strings/translations_en.properties +++ b/lang/proc/strings/translations_en.properties @@ -152,6 +152,7 @@ shells=Available shells startContainer=Start container stopContainer=Stop container inspectContainer=Inspect container +inspectContext=Inspect context k8sClusterNameDescription=The name of the context the cluster is in. pod=Pod podName=Pod name @@ -323,4 +324,13 @@ rdpUsernameDescription=To user to log in as addressDescription=Where to connect to rdpAdditionalOptions=Additional RDP options rdpAdditionalOptionsDescription=Raw RDP options to include, formatted the same as in .rdp files - +proxmoxVncConfirmTitle=VNC setup +proxmoxVncConfirmHeader=Do you want to enable VNC for the virtual machine? +proxmoxVncConfirmContent=This will set up an accessible VNC server and restart the virtual machine. You should then wait until the machine has started up again before connecting. +dockerContext.displayName=Docker context +dockerContext.displayDescription=Interact with containers located in a specific context +containerActions=Container actions +vmActions=VM actions +dockerContextActions=Context actions +k8sPodActions=Pod actions +openVnc=Set up VNC \ No newline at end of file diff --git a/lang/proc/strings/translations_es.properties b/lang/proc/strings/translations_es.properties index 2b60b2ce..3f92e906 100644 --- a/lang/proc/strings/translations_es.properties +++ b/lang/proc/strings/translations_es.properties @@ -152,6 +152,7 @@ shells=Conchas disponibles startContainer=Contenedor de inicio stopContainer=Contenedor de parada inspectContainer=Inspeccionar contenedor +inspectContext=Inspeccionar contexto k8sClusterNameDescription=El nombre del contexto en el que se encuentra el clúster. pod=Pod podName=Nombre del pod @@ -321,3 +322,12 @@ rdpUsernameDescription=Para que el usuario inicie sesión como addressDescription=Dónde conectarse rdpAdditionalOptions=Opciones RDP adicionales rdpAdditionalOptionsDescription=Opciones RDP en bruto a incluir, con el mismo formato que en los archivos .rdp +proxmoxVncConfirmTitle=Configuración VNC +proxmoxVncConfirmHeader=¿Quieres activar VNC para la máquina virtual? +proxmoxVncConfirmContent=Esto configurará un servidor VNC accesible y reiniciará la máquina virtual. Deberás esperar a que la máquina se haya reiniciado antes de conectarte. +dockerContext.displayName=Contexto Docker +dockerContext.displayDescription=Interactúa con contenedores situados en un contexto específico +containerActions=Acciones del contenedor +vmActions=Acciones VM +dockerContextActions=Acciones contextuales +k8sPodActions=Acciones del pod diff --git a/lang/proc/strings/translations_fr.properties b/lang/proc/strings/translations_fr.properties index c9bc8896..94b1521f 100644 --- a/lang/proc/strings/translations_fr.properties +++ b/lang/proc/strings/translations_fr.properties @@ -152,6 +152,7 @@ shells=Coquilles disponibles startContainer=Conteneur de départ stopContainer=Arrêter le conteneur inspectContainer=Inspecter le conteneur +inspectContext=Inspecter le contexte k8sClusterNameDescription=Le nom du contexte dans lequel se trouve le cluster. pod=Cosse podName=Nom du pod @@ -321,3 +322,12 @@ rdpUsernameDescription=A l'utilisateur de se connecter en tant que addressDescription=Où se connecter rdpAdditionalOptions=Options RDP supplémentaires rdpAdditionalOptionsDescription=Options RDP brutes à inclure, formatées de la même manière que dans les fichiers .rdp +proxmoxVncConfirmTitle=Configuration de VNC +proxmoxVncConfirmHeader=Veux-tu activer VNC pour la machine virtuelle ? +proxmoxVncConfirmContent=Cela permet de mettre en place un serveur VNC accessible et de redémarrer la machine virtuelle. Tu dois alors attendre que la machine ait redémarré avant de te connecter. +dockerContext.displayName=Contexte Docker +dockerContext.displayDescription=Interagir avec des conteneurs situés dans un contexte spécifique +containerActions=Actions du conteneur +vmActions=Actions VM +dockerContextActions=Actions contextuelles +k8sPodActions=Actions de pods diff --git a/lang/proc/strings/translations_it.properties b/lang/proc/strings/translations_it.properties index e278243a..e6c884df 100644 --- a/lang/proc/strings/translations_it.properties +++ b/lang/proc/strings/translations_it.properties @@ -152,6 +152,7 @@ shells=Gusci disponibili startContainer=Contenitore iniziale stopContainer=Contenitore di arresto inspectContainer=Ispezionare il contenitore +inspectContext=Ispezionare il contesto k8sClusterNameDescription=Il nome del contesto in cui si trova il cluster. pod=Pod podName=Nome del pod @@ -321,3 +322,12 @@ rdpUsernameDescription=All'utente di accedere come addressDescription=Dove connettersi rdpAdditionalOptions=Opzioni RDP aggiuntive rdpAdditionalOptionsDescription=Opzioni RDP grezze da includere, formattate come nei file .rdp +proxmoxVncConfirmTitle=Configurazione di VNC +proxmoxVncConfirmHeader=Vuoi abilitare VNC per la macchina virtuale? +proxmoxVncConfirmContent=In questo modo verrà configurato un server VNC accessibile e verrà riavviata la macchina virtuale. Dovrai quindi attendere che la macchina si sia riavviata prima di connetterti. +dockerContext.displayName=Contesto Docker +dockerContext.displayDescription=Interagire con contenitori situati in un contesto specifico +containerActions=Azioni del contenitore +vmActions=Azioni della VM +dockerContextActions=Azioni contestuali +k8sPodActions=Azioni del pod diff --git a/lang/proc/strings/translations_ja.properties b/lang/proc/strings/translations_ja.properties index 50b79cbf..69ed375d 100644 --- a/lang/proc/strings/translations_ja.properties +++ b/lang/proc/strings/translations_ja.properties @@ -152,6 +152,7 @@ shells=利用可能なシェル startContainer=スタートコンテナ stopContainer=停止コンテナ inspectContainer=コンテナを検査する +inspectContext=コンテキストを検査する k8sClusterNameDescription=クラスタが存在するコンテキストの名前。 pod=ポッド podName=ポッド名 @@ -321,3 +322,12 @@ rdpUsernameDescription=としてログインする addressDescription=接続先 rdpAdditionalOptions=RDPの追加オプション rdpAdditionalOptionsDescription=.rdpファイルと同じ書式で、RDPの生オプションを含める。 +proxmoxVncConfirmTitle=VNCのセットアップ +proxmoxVncConfirmHeader=仮想マシンのVNCを有効にするか? +proxmoxVncConfirmContent=これでアクセス可能なVNCサーバーがセットアップされ、仮想マシンが再起動する。その後、マシンが再び起動するまで待ってから接続する。 +dockerContext.displayName=Dockerコンテキスト +dockerContext.displayDescription=特定のコンテキストにあるコンテナと対話する +containerActions=コンテナアクション +vmActions=VMアクション +dockerContextActions=コンテキストアクション +k8sPodActions=ポッドアクション diff --git a/lang/proc/strings/translations_nl.properties b/lang/proc/strings/translations_nl.properties index 6fd296fa..272f6960 100644 --- a/lang/proc/strings/translations_nl.properties +++ b/lang/proc/strings/translations_nl.properties @@ -152,6 +152,7 @@ shells=Beschikbare schelpen startContainer=Start container stopContainer=Stopcontainer inspectContainer=Container inspecteren +inspectContext=Context inspecteren k8sClusterNameDescription=De naam van de context waarin het cluster zich bevindt. pod=Pod podName=Pod naam @@ -321,3 +322,12 @@ rdpUsernameDescription=Naar gebruiker om in te loggen als addressDescription=Waar je verbinding mee moet maken rdpAdditionalOptions=Extra RDP opties rdpAdditionalOptionsDescription=Rauwe RDP-opties om op te nemen, in dezelfde opmaak als in .rdp-bestanden +proxmoxVncConfirmTitle=VNC-instelling +proxmoxVncConfirmHeader=Wil je VNC inschakelen voor de virtuele machine? +proxmoxVncConfirmContent=Hiermee wordt een toegankelijke VNC-server opgezet en wordt de virtuele machine opnieuw opgestart. Je moet dan wachten tot de machine weer is opgestart voordat je verbinding maakt. +dockerContext.displayName=Docker context +dockerContext.displayDescription=Interactie met containers in een specifieke context +containerActions=Container acties +vmActions=VM-acties +dockerContextActions=Context acties +k8sPodActions=Pod acties diff --git a/lang/proc/strings/translations_pt.properties b/lang/proc/strings/translations_pt.properties index 12026467..4dc225db 100644 --- a/lang/proc/strings/translations_pt.properties +++ b/lang/proc/strings/translations_pt.properties @@ -152,6 +152,7 @@ shells=Conchas disponíveis startContainer=Iniciar contentor stopContainer=Pára o contentor inspectContainer=Inspecciona o contentor +inspectContext=Inspecionar contexto k8sClusterNameDescription=O nome do contexto em que o cluster se encontra. pod=Pod podName=Nome do pod @@ -321,3 +322,12 @@ rdpUsernameDescription=Para que o utilizador inicie sessão como addressDescription=Onde te deves ligar rdpAdditionalOptions=Opções adicionais de RDP rdpAdditionalOptionsDescription=Opções RDP brutas a incluir, formatadas da mesma forma que nos ficheiros .rdp +proxmoxVncConfirmTitle=Configuração do VNC +proxmoxVncConfirmHeader=Pretendes ativar o VNC para a máquina virtual? +proxmoxVncConfirmContent=Isto irá configurar um servidor VNC acessível e reiniciar a máquina virtual. Deves esperar até que a máquina seja reiniciada antes de te ligares. +dockerContext.displayName=Contexto do Docker +dockerContext.displayDescription=Interage com contentores localizados num contexto específico +containerActions=Acções de contentor +vmActions=Acções VM +dockerContextActions=Acções de contexto +k8sPodActions=Acções de pod diff --git a/lang/proc/strings/translations_ru.properties b/lang/proc/strings/translations_ru.properties index f246c03b..3b6c79cf 100644 --- a/lang/proc/strings/translations_ru.properties +++ b/lang/proc/strings/translations_ru.properties @@ -152,6 +152,7 @@ shells=Доступные оболочки startContainer=Стартовый контейнер stopContainer=Контейнер для остановки inspectContainer=Осмотрите контейнер +inspectContext=Осмотрите контекст k8sClusterNameDescription=Название контекста, в котором находится кластер. pod=Под podName=Название капсулы @@ -321,3 +322,12 @@ rdpUsernameDescription=Чтобы пользователь вошел в сис addressDescription=К чему подключиться rdpAdditionalOptions=Дополнительные опции RDP rdpAdditionalOptionsDescription=Необработанные опции RDP, которые нужно включить, в том же формате, что и в файлах .rdp +proxmoxVncConfirmTitle=Настройка VNC +proxmoxVncConfirmHeader=Хочешь ли ты включить VNC для виртуальной машины? +proxmoxVncConfirmContent=Это настроит доступный VNC-сервер и перезапустит виртуальную машину. Затем тебе следует подождать, пока машина снова запустится, прежде чем подключаться. +dockerContext.displayName=Контекст Docker +dockerContext.displayDescription=Взаимодействуй с контейнерами, расположенными в определенном контексте +containerActions=Действия с контейнером +vmActions=Действия виртуальной машины +dockerContextActions=Контекстные действия +k8sPodActions=Действия в капсуле diff --git a/lang/proc/strings/translations_tr.properties b/lang/proc/strings/translations_tr.properties index a52b70c9..663fd0b3 100644 --- a/lang/proc/strings/translations_tr.properties +++ b/lang/proc/strings/translations_tr.properties @@ -152,6 +152,7 @@ shells=Mevcut kabuklar startContainer=Konteyneri başlat stopContainer=Konteyneri durdur inspectContainer=Konteyneri inceleyin +inspectContext=Bağlamı inceleyin k8sClusterNameDescription=Kümenin içinde bulunduğu bağlamın adı. pod=Pod podName=Bölme adı @@ -321,3 +322,12 @@ rdpUsernameDescription=Kullanıcı olarak oturum açmak için addressDescription=Nereye bağlanmalı rdpAdditionalOptions=Ek RDP seçenekleri rdpAdditionalOptionsDescription=Dahil edilecek ham RDP seçenekleri, .rdp dosyalarında olduğu gibi biçimlendirilir +proxmoxVncConfirmTitle=VNC kurulumu +proxmoxVncConfirmHeader=Sanal makine için VNC'yi etkinleştirmek istiyor musunuz? +proxmoxVncConfirmContent=Bu, erişilebilir bir VNC sunucusu kuracak ve sanal makineyi yeniden başlatacaktır. Daha sonra bağlanmadan önce makinenin tekrar başlamasını beklemelisiniz. +dockerContext.displayName=Docker bağlamı +dockerContext.displayDescription=Belirli bir bağlamda bulunan konteynerlerle etkileşim +containerActions=Konteyner eylemleri +vmActions=VM eylemleri +dockerContextActions=Bağlam eylemleri +k8sPodActions=Pod eylemleri diff --git a/lang/proc/strings/translations_zh.properties b/lang/proc/strings/translations_zh.properties index a332d60f..412fab15 100644 --- a/lang/proc/strings/translations_zh.properties +++ b/lang/proc/strings/translations_zh.properties @@ -152,6 +152,7 @@ shells=可用外壳 startContainer=启动容器 stopContainer=停止容器 inspectContainer=检查容器 +inspectContext=检查上下文 k8sClusterNameDescription=群组所处上下文的名称。 pod=花苞 podName=舱名 @@ -321,3 +322,12 @@ rdpUsernameDescription=用户以 addressDescription=连接到哪里 rdpAdditionalOptions=其他 RDP 选项 rdpAdditionalOptionsDescription=要包含的原始 RDP 选项,格式与 .rdp 文件相同 +proxmoxVncConfirmTitle=VNC 设置 +proxmoxVncConfirmHeader=您要为虚拟机启用 VNC 吗? +proxmoxVncConfirmContent=这将设置一个可访问的 VNC 服务器,并重新启动虚拟机。然后,您应等待虚拟机再次启动后再进行连接。 +dockerContext.displayName=Docker 上下文 +dockerContext.displayDescription=与位于特定环境中的容器交互 +containerActions=容器操作 +vmActions=虚拟机操作 +dockerContextActions=上下文操作 +k8sPodActions=Pod 操作 diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 00000000..3a380f40 --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,417 @@ +openapi: 3.0.1 +info: + title: XPipe API Documentation + description: | + The XPipe API provides programmatic access to XPipe’s features. + + The XPipe application will start up an HTTP server that can be used to send requests. + You can change the port of it in the settings menu. + Note that this server is HTTP-only for now as it runs only on localhost. HTTPS requests are not accepted. + + This allows you to programmatically manage remote systems. + 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. + If a required password is not stored and is set to be dynamically prompted, the running XPipe application will ask you to enter any required passwords. + + You can quickly get started by either using this page as an API reference or alternatively import the [OpenAPI definition file](/openapi.yaml) into your API client of choice. + See the authentication handshake below on how to authenticate prior to sending requests. + termsOfService: https://docs.xpipe.io/terms-of-service + contact: + name: XPipe - Contact us + url: mailto:hello@xpipe.io + version: "10.0" +externalDocs: + description: XPipe - Plans and pricing + url: https://xpipe.io/pricing +servers: + - url: http://localhost:21723 + description: XPipe Daemon API +paths: + /handshake: + post: + summary: Establish a new API session + description: | + Prior to sending requests to the API, you first have to establish a new API session via the handshake endpoint. + In the response you will receive a session token that you can use to authenticate during this session. + + This is done so that the daemon knows what kind of clients are connected and can manage individual capabilities for clients. + + Note that for development you can also turn off the required authentication in the XPipe settings menu, allowing you to send unauthenticated requests. + operationId: handshake + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/HandshakeRequest' + examples: + standard: + summary: Standard 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" } } + 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. + content: + application/json: + schema: + $ref: '#/components/schemas/HandshakeResponse' + 400: + $ref: '#/components/responses/BadRequest' + 500: + $ref: '#/components/responses/InternalServerError' + /connection/query: + post: + summary: Query connections + description: | + Queries all connections using various filters. + + The filters support globs and can match the category names and connection names. + All matching is case insensitive. + operationId: connectionQuery + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ConnectionQueryRequest' + examples: + all: + summary: All + value: { "categoryFilter": "*", "connectionFilter": "*", "typeFilter": "*" } + simple: + summary: Simple filter + value: { "categoryFilter": "default", "connectionFilter": "local machine", "typeFilter": "*" } + globs: + summary: Globs + value: { "categoryFilter": "*", "connectionFilter": "*/podman/*", "typeFilter": "*" } + responses: + 200: + description: The query was successful. The body contains all matched connections. + content: + application/json: + schema: + $ref: '#/components/schemas/ConnectionQueryResponse' + examples: + standard: + summary: Matched connections + value: { "found": [ { "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b", "category": ["default"] , + "connection": ["local machine"], "type": "local" }, + { "uuid": "e1462ddc-9beb-484c-bd91-bb666027e300", "category": ["default", "category 1"], + "connection": ["ssh system", "shell environments", "bash"], "type": "shellEnvironment" } ] } + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 500: + $ref: '#/components/responses/InternalServerError' + /shell/start: + post: + summary: Start shell connection + description: | + Starts a new shell session for a connection. If an existing shell session is already running for that connection, this operation will do nothing. + + Note that there are a variety of possible errors that can occur here when establishing the shell connection. + These errors will be returned with the HTTP return code 500. + operationId: shellStart + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ShellStartRequest' + examples: + local: + summary: Start local shell + value: { "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b" } + responses: + 200: + description: The operation was successful. The shell session was started. + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 500: + $ref: '#/components/responses/InternalServerError' + /shell/stop: + post: + summary: Stop shell connection + description: | + Stops an existing shell session for a connection. + + This operation will return once the shell has exited. + If the shell is busy or stuck, you might have to work with timeouts to account for these cases. + operationId: shellStop + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ShellStopRequest' + examples: + local: + summary: Stop local shell + value: { "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b" } + responses: + 200: + description: The operation was successful. The shell session was stopped. + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 500: + $ref: '#/components/responses/InternalServerError' + /shell/exec: + post: + summary: Execute command in a shell session + description: | + Runs a command in an active shell session and waits for it to finish. The exit code and output will be returned in the response. + + Note that a variety of different errors can occur when executing the command. + If the command finishes, even with an error code, a normal HTTP 200 response will be returned. + However, if any other error occurs like the shell not responding or exiting unexpectedly, an HTTP 500 response will be returned. + operationId: shellExec + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ShellExecRequest' + examples: + user: + summary: echo $USER + value: { "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b", "command": "echo $USER" } + invalid: + summary: invalid + value: { "uuid": "f0ec68aa-63f5-405c-b178-9a4454556d6b", "command": "invalid" } + responses: + 200: + description: The operation was successful. The shell command finished. + content: + application/json: + schema: + $ref: '#/components/schemas/ShellExecResponse' + examples: + user: + summary: echo $USER + value: { "exitCode": 0, "stdout": "root", "stderr": "" } + fail: + summary: invalid + value: { "exitCode": 127, "stdout": "", "stderr": "invalid: command not found" } + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 500: + $ref: '#/components/responses/InternalServerError' +components: + schemas: + ShellStartRequest: + type: object + properties: + connection: + type: string + description: The connection uuid + required: + - connection + ShellStopRequest: + type: object + properties: + connection: + type: string + description: The connection uuid + required: + - connection + ShellExecRequest: + type: object + properties: + connection: + type: string + description: The connection uuid + command: + type: string + description: The command to execute + required: + - connection + - command + ShellExecResponse: + type: object + properties: + exitCode: + type: integer + description: The exit code of the command + stdout: + type: string + description: The stdout output of the command + stderr: + type: string + description: The stderr output of the command + required: + - exitCode + - stdout + - stderr + ConnectionQueryRequest: + type: object + properties: + categoryFilter: + type: string + description: The filter string to match categories. Categories are delimited by / if they are hierarchical. The filter supports globs. + connectionFilter: + type: string + description: The filter string to match connection names. Connection names are delimited by / if they are hierarchical. The filter supports globs. + typeFilter: + type: string + description: The filter string to connection types. Every unique type of connection like SSH or docker has its own type identifier that you can match. The filter supports globs. + required: + - categoryFilter + - connectionFilter + - typeFilter + ConnectionQueryResponse: + type: object + properties: + found: + type: array + description: The found connections + items: + type: object + properties: + uuid: + type: string + description: The unique id of the connection + category: + type: array + description: The full category path as an array + items: + type: string + description: Individual category name + connection: + type: array + description: The full connection name path as an array + items: + type: string + description: Individual connection name + type: + type: string + description: The type identifier of the connection + required: + - uuid + - category + - connection + - type + required: + - found + HandshakeRequest: + type: object + properties: + auth: + $ref: '#/components/schemas/AuthMethod' + client: + $ref: '#/components/schemas/ClientInformation' + required: + - auth + - client + HandshakeResponse: + type: object + properties: + sessionToken: + type: string + description: The generated bearer token that can be used for authentication in this session + required: + - sessionToken + AuthMethod: + type: object + discriminator: + propertyName: type + properties: + type: + type: string + required: + - type + oneOf: + - $ref: '#/components/schemas/ApiKey' + - $ref: '#/components/schemas/Local' + ApiKey: + description: API key authentication + allOf: + - $ref: '#/components/schemas/AuthMethod' + - type: object + properties: + key: + type: string + description: The API key + required: + - key + Local: + description: Authentication method for local applications. Uses file system access as proof of authentication. + allOf: + - $ref: '#/components/schemas/AuthMethod' + - type: object + properties: + authFileContent: + type: string + description: The contents of the local file $TEMP/xpipe_auth. This file is automatically generated when XPipe starts. + required: + - authFileContent + ClientInformation: + type: object + discriminator: + propertyName: type + properties: + type: + type: string + required: + - type + ApiClientInformation: + description: Provides information about the client that connected to the XPipe API. + allOf: + - $ref: '#/components/schemas/ClientInformation' + - type: object + properties: + name: + type: string + description: The name of the client. + required: + - name + responses: + Success: + description: The action was successfully performed. + BadRequest: + description: Bad request. Please check error message and your parameters. + Unauthorized: + description: Authorization failed. Please supply a `Bearer` token via + the `Authorization` header. + Forbidden: + description: Authorization failed. Please supply a valid `Bearer` token via + the `Authorization` header. + NotFound: + description: The requested resource could not be found. + InternalServerError: + description: Internal error. + securitySchemes: + bearerAuth: + type: http + scheme: bearer + description: The bearer token used is the session token that you receive from the handshake exchange. +security: + - bearerAuth: [] diff --git a/version b/version index cdebdcf6..a3a9b449 100644 --- a/version +++ b/version @@ -1 +1 @@ -9.4-3 +10.0-1