mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-21 23:20:23 +00:00
Merge branch release-10.0 into master
This commit is contained in:
parent
bc9b962be9
commit
4426eb2424
343 changed files with 9172 additions and 4358 deletions
|
@ -45,20 +45,20 @@ dependencies {
|
||||||
api 'com.vladsch.flexmark:flexmark-ext-footnotes:0.64.8'
|
api 'com.vladsch.flexmark:flexmark-ext-footnotes:0.64.8'
|
||||||
api 'com.vladsch.flexmark:flexmark-ext-definition:0.64.8'
|
api 'com.vladsch.flexmark:flexmark-ext-definition:0.64.8'
|
||||||
api 'com.vladsch.flexmark:flexmark-ext-anchorlink: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/markdowngenerator-1.3.1.1.jar")
|
||||||
api files("$rootDir/gradle/gradle_scripts/vernacular-1.16.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') {
|
api ('org.kohsuke:github-api:1.321') {
|
||||||
exclude group: 'org.apache.commons', module: 'commons-lang3'
|
exclude group: 'org.apache.commons', module: 'commons-lang3'
|
||||||
}
|
}
|
||||||
api 'org.apache.commons:commons-lang3:3.14.0'
|
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 '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.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-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-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-materialdesign2-pack', version: "12.2.0"
|
||||||
api group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0"
|
api group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0"
|
||||||
|
@ -68,10 +68,7 @@ dependencies {
|
||||||
api group: 'org.slf4j', name: 'slf4j-jdk-platform-logging', version: '2.0.13'
|
api group: 'org.slf4j', name: 'slf4j-jdk-platform-logging', version: '2.0.13'
|
||||||
api 'io.xpipe:modulefs:0.1.5'
|
api 'io.xpipe:modulefs:0.1.5'
|
||||||
api 'net.synedra:validatorfx:0.4.2'
|
api 'net.synedra:validatorfx:0.4.2'
|
||||||
api ('io.github.mkpaz:atlantafx-base:2.0.1') {
|
api files("$rootDir/gradle/gradle_scripts/atlantafx-base-2.0.2.jar")
|
||||||
exclude group: 'org.openjfx', module: 'javafx-base'
|
|
||||||
exclude group: 'org.openjfx', module: 'javafx-controls'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/gradle/gradle_scripts/local_junit_suite.gradle"
|
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.logLevel', "trace"
|
||||||
systemProperty 'io.xpipe.app.fullVersion', rootProject.fullVersion
|
systemProperty 'io.xpipe.app.fullVersion', rootProject.fullVersion
|
||||||
systemProperty 'io.xpipe.app.staging', isStage
|
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
|
// Apply passed xpipe properties
|
||||||
for (final def e in System.getProperties().entrySet()) {
|
for (final def e in System.getProperties().entrySet()) {
|
||||||
|
@ -153,6 +147,13 @@ processResources {
|
||||||
into resourcesDir
|
into resourcesDir
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
copy {
|
||||||
|
from file("$rootDir/openapi.yaml")
|
||||||
|
into file("${sourceSets.main.output.resourcesDir}/io/xpipe/app/resources/misc");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
distTar {
|
distTar {
|
||||||
|
|
174
app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java
Normal file
174
app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java
Normal file
|
@ -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<BeaconSession> sessions = new HashSet<>();
|
||||||
|
@Getter
|
||||||
|
private final Set<BeaconShellSession> shellSessions = new HashSet<>();
|
||||||
|
@Getter
|
||||||
|
private String localAuthSecret;
|
||||||
|
|
||||||
|
private String notFoundHtml;
|
||||||
|
private final Map<String, String> 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" +
|
||||||
|
"<link rel=\"stylesheet\" href=\"markdown.css\">" + "\n" +
|
||||||
|
"<link rel=\"stylesheet\" href=\"github-dark.min.css\">" + "\n" +
|
||||||
|
"<script src=\"highlight.min.js\"></script>" + "\n" +
|
||||||
|
"<script>hljs.highlightAll();</script>";
|
||||||
|
}, s -> {
|
||||||
|
return "<div style=\"max-width: 800px;margin: auto;\">" + s + "</div>";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var body = notFoundHtml.getBytes(StandardCharsets.UTF_8);
|
||||||
|
exchange.sendResponseHeaders(200,body.length);
|
||||||
|
try (var out = exchange.getResponseBody()) {
|
||||||
|
out.write(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
124
app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java
Normal file
124
app/src/main/java/io/xpipe/app/beacon/BeaconRequestHandler.java
Normal file
|
@ -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<T> implements HttpHandler {
|
||||||
|
|
||||||
|
private final BeaconInterface<T> beaconInterface;
|
||||||
|
|
||||||
|
public BeaconRequestHandler(BeaconInterface<T> 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> 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));
|
||||||
|
}
|
||||||
|
}
|
11
app/src/main/java/io/xpipe/app/beacon/BeaconSession.java
Normal file
11
app/src/main/java/io/xpipe/app/beacon/BeaconSession.java
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<DataStoreEntry> 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<QueryResponse>();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.app.core.AppProperties;
|
||||||
import io.xpipe.beacon.BeaconHandler;
|
import io.xpipe.beacon.BeaconClientException;
|
||||||
import io.xpipe.beacon.exchange.cli.VersionExchange;
|
import io.xpipe.beacon.BeaconServerException;
|
||||||
|
import io.xpipe.beacon.api.DaemonVersionExchange;
|
||||||
|
|
||||||
public class VersionExchangeImpl extends VersionExchange
|
import java.io.IOException;
|
||||||
implements MessageExchangeImpl<VersionExchange.Request, VersionExchange.Response> {
|
|
||||||
|
public class DaemonVersionExchangeImpl extends DaemonVersionExchange {
|
||||||
|
|
||||||
@Override
|
@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") + " "
|
var jvmVersion = System.getProperty("java.vm.vendor") + " "
|
||||||
+ System.getProperty("java.vm.name") + " ("
|
+ System.getProperty("java.vm.name") + " ("
|
||||||
+ System.getProperty("java.vm.version") + ")";
|
+ System.getProperty("java.vm.version") + ")";
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String> out = new AtomicReference<>();
|
||||||
|
AtomicReference<String> 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,6 @@ import io.xpipe.app.fxcomps.augment.DragOverPseudoClassAugment;
|
||||||
import io.xpipe.app.fxcomps.impl.*;
|
import io.xpipe.app.fxcomps.impl.*;
|
||||||
import io.xpipe.app.fxcomps.util.DerivedObservableList;
|
import io.xpipe.app.fxcomps.util.DerivedObservableList;
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
import io.xpipe.core.process.OsType;
|
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
|
@ -183,13 +182,7 @@ public class BrowserTransferComp extends SimpleComp {
|
||||||
event.consume();
|
event.consume();
|
||||||
});
|
});
|
||||||
struc.get().setOnDragDone(event -> {
|
struc.get().setOnDragDone(event -> {
|
||||||
// macOS does always report false here, which is unfortunate
|
model.clear();
|
||||||
if (!event.isAccepted() && !OsType.getLocal().equals(OsType.MACOS)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't clear, it might be more convenient to keep the contents
|
|
||||||
// model.clear();
|
|
||||||
event.consume();
|
event.consume();
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -27,8 +27,9 @@ public class BrowserEntry {
|
||||||
if (rawFileEntry == null) {
|
if (rawFileEntry == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
rawFileEntry = rawFileEntry.resolved();
|
||||||
|
|
||||||
if (rawFileEntry.getKind() == FileKind.DIRECTORY) {
|
if (rawFileEntry.getKind() != FileKind.FILE) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +46,7 @@ public class BrowserEntry {
|
||||||
if (rawFileEntry == null) {
|
if (rawFileEntry == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
rawFileEntry = rawFileEntry.resolved();
|
||||||
|
|
||||||
if (rawFileEntry.getKind() != FileKind.DIRECTORY) {
|
if (rawFileEntry.getKind() != FileKind.DIRECTORY) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -58,13 +60,14 @@ public class BrowserEntry {
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getIcon() {
|
public String getIcon() {
|
||||||
if (fileType != null) {
|
if (fileType != null) {
|
||||||
return fileType.getIcon();
|
return fileType.getIcon();
|
||||||
} else if (directoryType != null) {
|
} else if (directoryType != null) {
|
||||||
return directoryType.getIcon(rawFileEntry, false);
|
return directoryType.getIcon(rawFileEntry, false);
|
||||||
} else {
|
} else {
|
||||||
return rawFileEntry.getKind() == FileKind.DIRECTORY
|
return rawFileEntry != null && rawFileEntry.resolved().getKind() == FileKind.DIRECTORY
|
||||||
? "default_folder.svg"
|
? "default_folder.svg"
|
||||||
: "default_file.svg";
|
: "default_file.svg";
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import io.xpipe.core.store.FileKind;
|
||||||
import io.xpipe.core.store.FileSystem;
|
import io.xpipe.core.store.FileSystem;
|
||||||
import io.xpipe.core.store.LocalStore;
|
import io.xpipe.core.store.LocalStore;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
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) {
|
if (localFileSystem == null) {
|
||||||
throw new IllegalStateException();
|
throw new IllegalStateException();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new FileSystem.FileEntry(
|
return new FileSystem.FileEntry(
|
||||||
localFileSystem,
|
localFileSystem.open(),
|
||||||
file.toString(),
|
file.toString(),
|
||||||
Files.getLastModifiedTime(file).toInstant(),
|
Files.getLastModifiedTime(file).toInstant(),
|
||||||
Files.isHidden(file),
|
Files.isHidden(file),
|
||||||
|
|
|
@ -23,7 +23,7 @@ public abstract class BrowserSessionTab<T extends DataStore> {
|
||||||
this.browserModel = browserModel;
|
this.browserModel = browserModel;
|
||||||
this.entry = entry;
|
this.entry = entry;
|
||||||
this.name = DataStorage.get().getStoreDisplayName(entry.get());
|
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();
|
public abstract Comp<?> comp();
|
||||||
|
|
|
@ -13,6 +13,7 @@ import javafx.scene.control.ScrollPane;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -67,7 +68,9 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
||||||
// Clear cache of unused values
|
// Clear cache of unused values
|
||||||
cache.keySet().removeIf(t -> !all.contains(t));
|
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 -> {
|
.map(v -> {
|
||||||
if (!cache.containsKey(v)) {
|
if (!cache.containsKey(v)) {
|
||||||
var comp = compFunction.apply(v);
|
var comp = compFunction.apply(v);
|
||||||
|
@ -80,6 +83,10 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
if (listView.getChildren().equals(newShown)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < newShown.size(); i++) {
|
for (int i = 0; i < newShown.size(); i++) {
|
||||||
var r = newShown.get(i);
|
var r = newShown.get(i);
|
||||||
r.pseudoClassStateChanged(ODD, false);
|
r.pseudoClassStateChanged(ODD, false);
|
||||||
|
@ -87,10 +94,8 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
||||||
r.pseudoClassStateChanged(i % 2 == 0 ? EVEN : ODD, true);
|
r.pseudoClassStateChanged(i % 2 == 0 ? EVEN : ODD, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!listView.getChildren().equals(newShown)) {
|
var d = new DerivedObservableList<>(listView.getChildren(), true);
|
||||||
var d = new DerivedObservableList<>(listView.getChildren(), true);
|
d.setContent(newShown);
|
||||||
d.setContent(newShown);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (asynchronous) {
|
if (asynchronous) {
|
||||||
|
|
|
@ -42,7 +42,7 @@ public class MarkdownComp extends Comp<CompStructure<StackPane>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getHtml() {
|
private String getHtml() {
|
||||||
return MarkdownHelper.toHtml(markdown.getValue(), htmlTransformation);
|
return MarkdownHelper.toHtml(markdown.getValue(), s -> s, htmlTransformation);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
|
@ -55,8 +55,8 @@ public class MarkdownComp extends Comp<CompStructure<StackPane>> {
|
||||||
AppProperties.get().getDataDir().resolve("webview").toFile());
|
AppProperties.get().getDataDir().resolve("webview").toFile());
|
||||||
wv.setPageFill(Color.TRANSPARENT);
|
wv.setPageFill(Color.TRANSPARENT);
|
||||||
var theme = AppPrefs.get() != null && AppPrefs.get().theme.getValue().isDark()
|
var theme = AppPrefs.get() != null && AppPrefs.get().theme.getValue().isDark()
|
||||||
? "web/github-markdown-dark.css"
|
? "misc/github-markdown-dark.css"
|
||||||
: "web/github-markdown-light.css";
|
: "misc/github-markdown-light.css";
|
||||||
var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, theme).orElseThrow();
|
var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, theme).orElseThrow();
|
||||||
wv.getEngine().setUserStyleSheetLocation(url.toString());
|
wv.getEngine().setUserStyleSheetLocation(url.toString());
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import io.xpipe.app.fxcomps.augment.Augment;
|
||||||
import io.xpipe.app.fxcomps.impl.IconButtonComp;
|
import io.xpipe.app.fxcomps.impl.IconButtonComp;
|
||||||
import io.xpipe.app.fxcomps.impl.TooltipAugment;
|
import io.xpipe.app.fxcomps.impl.TooltipAugment;
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
|
import io.xpipe.app.prefs.AppPrefs;
|
||||||
import io.xpipe.app.update.UpdateAvailableAlert;
|
import io.xpipe.app.update.UpdateAvailableAlert;
|
||||||
import io.xpipe.app.update.XPipeDistributionType;
|
import io.xpipe.app.update.XPipeDistributionType;
|
||||||
import io.xpipe.app.util.Hyperlinks;
|
import io.xpipe.app.util.Hyperlinks;
|
||||||
|
@ -147,9 +148,8 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
var shortcut = new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 2]);
|
|
||||||
var b = new IconButtonComp("mdi2t-translate", () -> Hyperlinks.open(Hyperlinks.TRANSLATE))
|
var b = new IconButtonComp("mdi2t-translate", () -> Hyperlinks.open(Hyperlinks.TRANSLATE))
|
||||||
.tooltipKey("translate", shortcut)
|
.tooltipKey("translate")
|
||||||
.apply(simpleBorders)
|
.apply(simpleBorders)
|
||||||
.accessibleTextKey("translate");
|
.accessibleTextKey("translate");
|
||||||
b.apply(struc -> {
|
b.apply(struc -> {
|
||||||
|
@ -158,6 +158,17 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
||||||
vbox.getChildren().add(b.createRegion());
|
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())
|
var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableAlert.showIfNeeded())
|
||||||
.tooltipKey("updateAvailableTooltip")
|
.tooltipKey("updateAvailableTooltip")
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
package io.xpipe.app.comp.base;
|
package io.xpipe.app.comp.base;
|
||||||
|
|
||||||
import io.xpipe.app.comp.store.StoreSection;
|
import io.xpipe.app.comp.store.StoreSection;
|
||||||
|
import io.xpipe.app.comp.store.StoreViewState;
|
||||||
import io.xpipe.app.core.AppI18n;
|
import io.xpipe.app.core.AppI18n;
|
||||||
import io.xpipe.app.fxcomps.SimpleComp;
|
import io.xpipe.app.fxcomps.SimpleComp;
|
||||||
|
import io.xpipe.app.fxcomps.util.LabelGraphic;
|
||||||
import io.xpipe.app.storage.DataStoreEntry;
|
import io.xpipe.app.storage.DataStoreEntry;
|
||||||
import io.xpipe.app.util.ThreadHelper;
|
import io.xpipe.app.util.ThreadHelper;
|
||||||
import io.xpipe.core.store.DataStore;
|
import io.xpipe.core.store.DataStore;
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.beans.value.ObservableBooleanValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@ -24,17 +25,19 @@ import java.util.function.Function;
|
||||||
public class StoreToggleComp extends SimpleComp {
|
public class StoreToggleComp extends SimpleComp {
|
||||||
|
|
||||||
private final String nameKey;
|
private final String nameKey;
|
||||||
|
private final ObservableValue<LabelGraphic> graphic;
|
||||||
private final StoreSection section;
|
private final StoreSection section;
|
||||||
private final BooleanProperty value;
|
private final BooleanProperty value;
|
||||||
private final Consumer<Boolean> onChange;
|
private final Consumer<Boolean> onChange;
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
private ObservableBooleanValue customVisibility = new SimpleBooleanProperty(true);
|
private ObservableValue<Boolean> customVisibility = new SimpleBooleanProperty(true);
|
||||||
|
|
||||||
public static <T extends DataStore> StoreToggleComp simpleToggle(
|
public static <T extends DataStore> StoreToggleComp simpleToggle(
|
||||||
String nameKey, StoreSection section, Function<T, Boolean> initial, BiConsumer<T, Boolean> setter) {
|
String nameKey, ObservableValue<LabelGraphic> graphic, StoreSection section, Function<T, Boolean> initial, BiConsumer<T, Boolean> setter) {
|
||||||
return new StoreToggleComp(
|
return new StoreToggleComp(
|
||||||
nameKey,
|
nameKey,
|
||||||
|
graphic,
|
||||||
section,
|
section,
|
||||||
new SimpleBooleanProperty(
|
new SimpleBooleanProperty(
|
||||||
initial.apply(section.getWrapper().getEntry().getStore().asNeeded())),
|
initial.apply(section.getWrapper().getEntry().getStore().asNeeded())),
|
||||||
|
@ -43,27 +46,57 @@ public class StoreToggleComp extends SimpleComp {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T extends DataStore> StoreToggleComp enableToggle(
|
||||||
|
String nameKey, StoreSection section, Function<T, Boolean> initial, BiConsumer<T, Boolean> setter) {
|
||||||
|
var val = new SimpleBooleanProperty();
|
||||||
|
ObservableValue<LabelGraphic> 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 <T extends DataStore> StoreToggleComp childrenToggle(
|
public static <T extends DataStore> StoreToggleComp childrenToggle(
|
||||||
String nameKey, StoreSection section, Function<T, Boolean> initial, BiConsumer<T, Boolean> setter) {
|
String nameKey, boolean graphic, StoreSection section, Function<T, Boolean> initial, BiConsumer<T, Boolean> setter) {
|
||||||
return new StoreToggleComp(
|
var val = new SimpleBooleanProperty();
|
||||||
nameKey,
|
ObservableValue<LabelGraphic> g = graphic ? val.map(aBoolean -> aBoolean ?
|
||||||
section,
|
new LabelGraphic.IconGraphic("mdi2c-circle-slice-8") : new LabelGraphic.IconGraphic("mdi2c-circle-half-full")) : null;
|
||||||
new SimpleBooleanProperty(
|
var t = new StoreToggleComp(nameKey, g, section,
|
||||||
initial.apply(section.getWrapper().getEntry().getStore().asNeeded())),
|
new SimpleBooleanProperty(initial.apply(section.getWrapper().getEntry().getStore().asNeeded())), v -> {
|
||||||
v -> {
|
Platform.runLater(() -> {
|
||||||
setter.accept(section.getWrapper().getEntry().getStore().asNeeded(), v);
|
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<Boolean> onChange) {
|
public StoreToggleComp(String nameKey, ObservableValue<LabelGraphic> graphic, StoreSection section, boolean initial, Consumer<Boolean> onChange) {
|
||||||
this.nameKey = nameKey;
|
this.nameKey = nameKey;
|
||||||
|
this.graphic = graphic;
|
||||||
this.section = section;
|
this.section = section;
|
||||||
this.value = new SimpleBooleanProperty(initial);
|
this.value = new SimpleBooleanProperty(initial);
|
||||||
this.onChange = onChange;
|
this.onChange = onChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
public StoreToggleComp(String nameKey, StoreSection section, BooleanProperty initial, Consumer<Boolean> onChange) {
|
public StoreToggleComp(String nameKey, ObservableValue<LabelGraphic> graphic, StoreSection section, BooleanProperty initial, Consumer<Boolean> onChange) {
|
||||||
this.nameKey = nameKey;
|
this.nameKey = nameKey;
|
||||||
|
this.graphic = graphic;
|
||||||
this.section = section;
|
this.section = section;
|
||||||
this.value = initial;
|
this.value = initial;
|
||||||
this.onChange = onChange;
|
this.onChange = onChange;
|
||||||
|
@ -74,7 +107,7 @@ public class StoreToggleComp extends SimpleComp {
|
||||||
var disable = section.getWrapper().getValidity().map(state -> state != DataStoreEntry.Validity.COMPLETE);
|
var disable = section.getWrapper().getValidity().map(state -> state != DataStoreEntry.Validity.COMPLETE);
|
||||||
var visible = Bindings.createBooleanBinding(
|
var visible = Bindings.createBooleanBinding(
|
||||||
() -> {
|
() -> {
|
||||||
if (!this.customVisibility.get()) {
|
if (!this.customVisibility.getValue()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +116,7 @@ public class StoreToggleComp extends SimpleComp {
|
||||||
section.getWrapper().getValidity(),
|
section.getWrapper().getValidity(),
|
||||||
section.getShowDetails(),
|
section.getShowDetails(),
|
||||||
this.customVisibility);
|
this.customVisibility);
|
||||||
var t = new ToggleSwitchComp(value, AppI18n.observable(nameKey))
|
var t = new ToggleSwitchComp(value, AppI18n.observable(nameKey), graphic)
|
||||||
.visible(visible)
|
.visible(visible)
|
||||||
.disable(disable);
|
.disable(disable);
|
||||||
value.addListener((observable, oldValue, newValue) -> {
|
value.addListener((observable, oldValue, newValue) -> {
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
package io.xpipe.app.comp.base;
|
package io.xpipe.app.comp.base;
|
||||||
|
|
||||||
|
import atlantafx.base.controls.ToggleSwitch;
|
||||||
import io.xpipe.app.fxcomps.SimpleComp;
|
import io.xpipe.app.fxcomps.SimpleComp;
|
||||||
|
import io.xpipe.app.fxcomps.util.LabelGraphic;
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
|
|
||||||
import javafx.beans.property.Property;
|
import javafx.beans.property.Property;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.css.PseudoClass;
|
||||||
import javafx.scene.input.KeyCode;
|
import javafx.scene.input.KeyCode;
|
||||||
import javafx.scene.input.KeyEvent;
|
import javafx.scene.input.KeyEvent;
|
||||||
import javafx.scene.layout.Region;
|
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 {
|
public class ToggleSwitchComp extends SimpleComp {
|
||||||
|
|
||||||
private final Property<Boolean> selected;
|
Property<Boolean> selected;
|
||||||
private final ObservableValue<String> name;
|
ObservableValue<String> name;
|
||||||
|
ObservableValue<LabelGraphic> graphic;
|
||||||
public ToggleSwitchComp(Property<Boolean> selected, ObservableValue<String> name) {
|
|
||||||
this.selected = selected;
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Region createSimple() {
|
protected Region createSimple() {
|
||||||
|
@ -43,6 +43,10 @@ public class ToggleSwitchComp extends SimpleComp {
|
||||||
if (name != null) {
|
if (name != null) {
|
||||||
s.textProperty().bind(PlatformThread.sync(name));
|
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;
|
return s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,7 +103,7 @@ public class DenseStoreEntryComp extends StoreEntryComp {
|
||||||
grid.getColumnConstraints().addAll(infoCC, custom);
|
grid.getColumnConstraints().addAll(infoCC, custom);
|
||||||
|
|
||||||
var cr = content != null ? content.createRegion() : new Region();
|
var cr = content != null ? content.createRegion() : new Region();
|
||||||
var bb = createButtonBar().createRegion();
|
var bb = createButtonBar();
|
||||||
var controls = new HBox(cr, bb);
|
var controls = new HBox(cr, bb);
|
||||||
controls.setFillHeight(true);
|
controls.setFillHeight(true);
|
||||||
controls.setAlignment(Pos.CENTER_RIGHT);
|
controls.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
|
|
@ -51,7 +51,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
|
||||||
var custom = new ColumnConstraints(0, customSize, customSize);
|
var custom = new ColumnConstraints(0, customSize, customSize);
|
||||||
custom.setHalignment(HPos.RIGHT);
|
custom.setHalignment(HPos.RIGHT);
|
||||||
var cr = content != null ? content.createRegion() : new Region();
|
var cr = content != null ? content.createRegion() : new Region();
|
||||||
var bb = createButtonBar().createRegion();
|
var bb = createButtonBar();
|
||||||
var controls = new HBox(cr, bb);
|
var controls = new HBox(cr, bb);
|
||||||
controls.setFillHeight(true);
|
controls.setFillHeight(true);
|
||||||
HBox.setHgrow(cr, Priority.ALWAYS);
|
HBox.setHgrow(cr, Priority.ALWAYS);
|
||||||
|
|
|
@ -286,6 +286,10 @@ public class StoreCreationComp extends DialogComp {
|
||||||
if (ex instanceof ValidationException) {
|
if (ex instanceof ValidationException) {
|
||||||
ErrorEvent.expected(ex);
|
ErrorEvent.expected(ex);
|
||||||
skippable.set(false);
|
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 {
|
} else {
|
||||||
skippable.set(true);
|
skippable.set(true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,12 +40,16 @@ public class StoreCreationMenu {
|
||||||
|
|
||||||
menu.getItems()
|
menu.getItems()
|
||||||
.add(category(
|
.add(category(
|
||||||
"addCommand", "mdi2c-code-greater-than", DataStoreProvider.CreationCategory.COMMAND, "cmd"));
|
"addService", "mdi2c-cloud-braces", DataStoreProvider.CreationCategory.SERVICE, null));
|
||||||
|
|
||||||
menu.getItems()
|
menu.getItems()
|
||||||
.add(category(
|
.add(category(
|
||||||
"addTunnel", "mdi2v-vector-polyline-plus", DataStoreProvider.CreationCategory.TUNNEL, null));
|
"addTunnel", "mdi2v-vector-polyline-plus", DataStoreProvider.CreationCategory.TUNNEL, null));
|
||||||
|
|
||||||
|
menu.getItems()
|
||||||
|
.add(category(
|
||||||
|
"addCommand", "mdi2c-code-greater-than", DataStoreProvider.CreationCategory.COMMAND, "cmd"));
|
||||||
|
|
||||||
menu.getItems()
|
menu.getItems()
|
||||||
.add(category("addDatabase", "mdi2d-database-plus", DataStoreProvider.CreationCategory.DATABASE, null));
|
.add(category("addDatabase", "mdi2d-database-plus", DataStoreProvider.CreationCategory.DATABASE, null));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package io.xpipe.app.comp.store;
|
package io.xpipe.app.comp.store;
|
||||||
|
|
||||||
|
import atlantafx.base.layout.InputGroup;
|
||||||
import atlantafx.base.theme.Styles;
|
import atlantafx.base.theme.Styles;
|
||||||
import io.xpipe.app.comp.base.LoadingOverlayComp;
|
import io.xpipe.app.comp.base.LoadingOverlayComp;
|
||||||
import io.xpipe.app.core.*;
|
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.SimpleCompStructure;
|
||||||
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
|
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
|
||||||
import io.xpipe.app.fxcomps.augment.GrowAugment;
|
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.BindingsHelper;
|
||||||
|
import io.xpipe.app.fxcomps.util.DerivedObservableList;
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
import io.xpipe.app.prefs.AppPrefs;
|
import io.xpipe.app.prefs.AppPrefs;
|
||||||
import io.xpipe.app.storage.DataStorage;
|
import io.xpipe.app.storage.DataStorage;
|
||||||
|
@ -199,63 +204,65 @@ public abstract class StoreEntryComp extends SimpleComp {
|
||||||
return stack;
|
return stack;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Comp<?> createButtonBar() {
|
protected Region createButtonBar() {
|
||||||
var list = new ArrayList<Comp<?>>();
|
var list = new DerivedObservableList<>(wrapper.getActionProviders(), false);
|
||||||
for (var p : wrapper.getActionProviders().entrySet()) {
|
var buttons = list.mapped(actionProvider -> {
|
||||||
var actionProvider = p.getKey().getDataStoreCallSite();
|
var button = buildButton(actionProvider);
|
||||||
if (!actionProvider.isMajor(wrapper.getEntry().ref())) {
|
return button != null ? button.createRegion() : null;
|
||||||
continue;
|
}).filtered(region -> region != null).getList();
|
||||||
}
|
|
||||||
|
|
||||||
var def = p.getKey().getDefaultDataStoreCallSite();
|
var ig = new InputGroup();
|
||||||
if (def != null && def.equals(wrapper.getDefaultActionProvider().getValue())) {
|
Runnable update = () -> {
|
||||||
continue;
|
var l = new ArrayList<Node>(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 =
|
private Comp<?> buildButton(ActionProvider p) {
|
||||||
new IconButtonComp(actionProvider.getIcon(wrapper.getEntry().ref()), () -> {
|
var leaf = p.getLeafDataStoreCallSite();
|
||||||
ThreadHelper.runFailableAsync(() -> {
|
var branch = p.getBranchDataStoreCallSite();
|
||||||
var action = actionProvider.createAction(
|
var cs = leaf != null ? leaf : branch;
|
||||||
wrapper.getEntry().ref());
|
|
||||||
action.execute();
|
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(
|
} : null);
|
||||||
actionProvider.getName(wrapper.getEntry().ref()).getValue());
|
if (branch != null) {
|
||||||
button.apply(new TooltipAugment<>(
|
button.apply(new ContextMenuAugment<>(mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY,keyEvent -> false,() -> {
|
||||||
actionProvider.getName(wrapper.getEntry().ref()), null));
|
var cm = ContextMenuHelper.create();
|
||||||
if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ONLY_SHOW_IF_ENABLED) {
|
branch.getChildren().forEach(childProvider -> {
|
||||||
button.hide(Bindings.not(p.getValue()));
|
var menu = buildMenuItemForAction(childProvider);
|
||||||
} else if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ALWAYS_SHOW) {
|
if (menu != null) {
|
||||||
button.disable(Bindings.not(p.getValue()));
|
cm.getItems().add(menu);
|
||||||
}
|
}
|
||||||
list.add(button);
|
});
|
||||||
|
return cm;
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
button.accessibleText(
|
||||||
var settingsButton = createSettingsButton();
|
cs.getName(wrapper.getEntry().ref()).getValue());
|
||||||
list.add(settingsButton);
|
button.apply(new TooltipAugment<>(
|
||||||
if (list.size() > 1) {
|
cs.getName(wrapper.getEntry().ref()), null));
|
||||||
list.getFirst().styleClass(Styles.LEFT_PILL);
|
return button;
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Comp<?> createSettingsButton() {
|
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.styleClass("settings");
|
||||||
settingsButton.accessibleText("More");
|
settingsButton.accessibleText("More");
|
||||||
settingsButton.apply(new ContextMenuAugment<>(
|
settingsButton.apply(new ContextMenuAugment<>(
|
||||||
|
@ -271,103 +278,21 @@ public abstract class StoreEntryComp extends SimpleComp {
|
||||||
AppFont.normal(contextMenu.getStyleableNode());
|
AppFont.normal(contextMenu.getStyleableNode());
|
||||||
|
|
||||||
var hasSep = false;
|
var hasSep = false;
|
||||||
for (var p : wrapper.getActionProviders().entrySet()) {
|
for (var p : wrapper.getActionProviders()) {
|
||||||
var actionProvider = p.getKey().getDataStoreCallSite();
|
var item = buildMenuItemForAction(p);
|
||||||
if (actionProvider.isMajor(wrapper.getEntry().ref())) {
|
if (item == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actionProvider.isSystemAction() && !hasSep) {
|
if (p.getLeafDataStoreCallSite() != null && p.getLeafDataStoreCallSite().isSystemAction() && !hasSep) {
|
||||||
if (contextMenu.getItems().size() > 0) {
|
if (contextMenu.getItems().size() > 0) {
|
||||||
contextMenu.getItems().add(new SeparatorMenuItem());
|
contextMenu.getItems().add(new SeparatorMenuItem());
|
||||||
}
|
}
|
||||||
hasSep = true;
|
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);
|
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) {
|
if (contextMenu.getItems().size() > 0 && !hasSep) {
|
||||||
contextMenu.getItems().add(new SeparatorMenuItem());
|
contextMenu.getItems().add(new SeparatorMenuItem());
|
||||||
}
|
}
|
||||||
|
@ -385,6 +310,11 @@ public abstract class StoreEntryComp extends SimpleComp {
|
||||||
browse.setOnAction(
|
browse.setOnAction(
|
||||||
event -> DesktopHelper.browsePathLocal(wrapper.getEntry().getDirectory()));
|
event -> DesktopHelper.browsePathLocal(wrapper.getEntry().getDirectory()));
|
||||||
contextMenu.getItems().add(browse);
|
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())) {
|
if (DataStorage.get().isRootEntry(wrapper.getEntry())) {
|
||||||
|
@ -418,7 +348,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
||||||
wrapper.moveTo(storeCategoryWrapper.getCategory());
|
wrapper.moveTo(storeCategoryWrapper.getCategory());
|
||||||
event.consume();
|
event.consume();
|
||||||
});
|
});
|
||||||
if (storeCategoryWrapper.getParent() == null) {
|
if (storeCategoryWrapper.getParent() == null || storeCategoryWrapper.equals(wrapper.getCategory().getValue())) {
|
||||||
m.setDisable(true);
|
m.setDisable(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -490,6 +420,94 @@ public abstract class StoreEntryComp extends SimpleComp {
|
||||||
return contextMenu;
|
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;
|
private static String DEFAULT_NOTES = null;
|
||||||
|
|
||||||
|
|
|
@ -61,14 +61,14 @@ public class StoreEntryListStatusComp extends SimpleComp {
|
||||||
var inRootCategory = StoreViewState.get().getActiveCategory().getValue().getRoot().equals(rootCategory);
|
var inRootCategory = StoreViewState.get().getActiveCategory().getValue().getRoot().equals(rootCategory);
|
||||||
// Sadly the all binding does not update when the individual visibility of entries changes
|
// Sadly the all binding does not update when the individual visibility of entries changes
|
||||||
// But it is good enough.
|
// But it is good enough.
|
||||||
var showProvider = storeEntryWrapper.getEntry().getProvider() == null ||
|
var showProvider = !storeEntryWrapper.getEntry().getValidity().isUsable() ||
|
||||||
storeEntryWrapper.getEntry().getProvider().shouldShow(storeEntryWrapper);
|
storeEntryWrapper.getEntry().getProvider().shouldShow(storeEntryWrapper);
|
||||||
return inRootCategory && showProvider;
|
return inRootCategory && showProvider;
|
||||||
},
|
},
|
||||||
StoreViewState.get().getActiveCategory());
|
StoreViewState.get().getActiveCategory());
|
||||||
var shownList = all.filtered(
|
var shownList = all.filtered(
|
||||||
storeEntryWrapper -> {
|
storeEntryWrapper -> {
|
||||||
return storeEntryWrapper.shouldShow(
|
return storeEntryWrapper.matchesFilter(
|
||||||
StoreViewState.get().getFilterString().getValue());
|
StoreViewState.get().getFilterString().getValue());
|
||||||
},
|
},
|
||||||
StoreViewState.get().getFilterString());
|
StoreViewState.get().getFilterString());
|
||||||
|
|
|
@ -9,8 +9,8 @@ import io.xpipe.app.storage.DataStoreCategory;
|
||||||
import io.xpipe.app.storage.DataStoreColor;
|
import io.xpipe.app.storage.DataStoreColor;
|
||||||
import io.xpipe.app.storage.DataStoreEntry;
|
import io.xpipe.app.storage.DataStoreEntry;
|
||||||
import io.xpipe.app.util.ThreadHelper;
|
import io.xpipe.app.util.ThreadHelper;
|
||||||
import javafx.beans.Observable;
|
|
||||||
import javafx.beans.property.*;
|
import javafx.beans.property.*;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
@ -26,8 +26,8 @@ public class StoreEntryWrapper {
|
||||||
private final BooleanProperty disabled = new SimpleBooleanProperty();
|
private final BooleanProperty disabled = new SimpleBooleanProperty();
|
||||||
private final BooleanProperty busy = new SimpleBooleanProperty();
|
private final BooleanProperty busy = new SimpleBooleanProperty();
|
||||||
private final Property<DataStoreEntry.Validity> validity = new SimpleObjectProperty<>();
|
private final Property<DataStoreEntry.Validity> validity = new SimpleObjectProperty<>();
|
||||||
private final Map<ActionProvider, BooleanProperty> actionProviders;
|
private final ListProperty<ActionProvider> actionProviders = new SimpleListProperty<>(FXCollections.observableArrayList());
|
||||||
private final Property<ActionProvider.DefaultDataStoreCallSite<?>> defaultActionProvider;
|
private final Property<ActionProvider> defaultActionProvider = new SimpleObjectProperty<>();
|
||||||
private final BooleanProperty deletable = new SimpleBooleanProperty();
|
private final BooleanProperty deletable = new SimpleBooleanProperty();
|
||||||
private final BooleanProperty expanded = new SimpleBooleanProperty();
|
private final BooleanProperty expanded = new SimpleBooleanProperty();
|
||||||
private final Property<Object> persistentState = new SimpleObjectProperty<>();
|
private final Property<Object> persistentState = new SimpleObjectProperty<>();
|
||||||
|
@ -41,30 +41,24 @@ public class StoreEntryWrapper {
|
||||||
this.entry = entry;
|
this.entry = entry;
|
||||||
this.name = new SimpleStringProperty(entry.getName());
|
this.name = new SimpleStringProperty(entry.getName());
|
||||||
this.lastAccess = new SimpleObjectProperty<>(entry.getLastAccess().minus(Duration.ofMillis(500)));
|
this.lastAccess = new SimpleObjectProperty<>(entry.getLastAccess().minus(Duration.ofMillis(500)));
|
||||||
this.actionProviders = new LinkedHashMap<>();
|
|
||||||
ActionProvider.ALL.stream()
|
ActionProvider.ALL.stream()
|
||||||
.filter(dataStoreActionProvider -> {
|
.filter(dataStoreActionProvider -> {
|
||||||
return !entry.isDisabled()
|
return !entry.isDisabled()
|
||||||
&& dataStoreActionProvider.getDataStoreCallSite() != null
|
&& dataStoreActionProvider.getLeafDataStoreCallSite() != null
|
||||||
&& dataStoreActionProvider
|
&& dataStoreActionProvider
|
||||||
.getDataStoreCallSite()
|
.getLeafDataStoreCallSite()
|
||||||
.getApplicableClass()
|
.getApplicableClass()
|
||||||
.isAssignableFrom(entry.getStore().getClass());
|
.isAssignableFrom(entry.getStore().getClass());
|
||||||
})
|
})
|
||||||
.sorted(Comparator.comparing(
|
.sorted(Comparator.comparing(
|
||||||
actionProvider -> actionProvider.getDataStoreCallSite().isSystemAction()))
|
actionProvider -> actionProvider.getLeafDataStoreCallSite().isSystemAction()))
|
||||||
.forEach(dataStoreActionProvider -> {
|
.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()));
|
this.notes = new SimpleObjectProperty<>(new StoreNotes(entry.getNotes(), entry.getNotes()));
|
||||||
setupListeners();
|
setupListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Observable> getUpdateObservables() {
|
|
||||||
return List.of(category);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void moveTo(DataStoreCategory category) {
|
public void moveTo(DataStoreCategory category) {
|
||||||
ThreadHelper.runAsync(() -> {
|
ThreadHelper.runAsync(() -> {
|
||||||
DataStorage.get().updateCategory(entry, category);
|
DataStorage.get().updateCategory(entry, category);
|
||||||
|
@ -136,7 +130,7 @@ public class StoreEntryWrapper {
|
||||||
color.setValue(entry.getColor());
|
color.setValue(entry.getColor());
|
||||||
notes.setValue(new StoreNotes(entry.getNotes(), entry.getNotes()));
|
notes.setValue(new StoreNotes(entry.getNotes(), entry.getNotes()));
|
||||||
|
|
||||||
busy.setValue(entry.isInRefresh());
|
busy.setValue(entry.getBusyCounter().get() != 0);
|
||||||
deletable.setValue(entry.getConfiguration().isDeletable()
|
deletable.setValue(entry.getConfiguration().isDeletable()
|
||||||
|| AppPrefs.get().developerDisableGuiRestrictions().getValue());
|
|| AppPrefs.get().developerDisableGuiRestrictions().getValue());
|
||||||
|
|
||||||
|
@ -156,48 +150,56 @@ public class StoreEntryWrapper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actionProviders.keySet().forEach(dataStoreActionProvider -> {
|
if (!isInStorage()) {
|
||||||
if (!isInStorage()) {
|
actionProviders.clear();
|
||||||
actionProviders.get(dataStoreActionProvider).set(false);
|
defaultActionProvider.setValue(null);
|
||||||
defaultActionProvider.setValue(null);
|
} else {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!entry.getValidity().isUsable()
|
|
||||||
&& !dataStoreActionProvider
|
|
||||||
.getDataStoreCallSite()
|
|
||||||
.activeType()
|
|
||||||
.equals(ActionProvider.DataStoreCallSite.ActiveType.ALWAYS_ENABLE)) {
|
|
||||||
actionProviders.get(dataStoreActionProvider).set(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaultProvider = ActionProvider.ALL.stream()
|
var defaultProvider = ActionProvider.ALL.stream()
|
||||||
.filter(e -> e.getDefaultDataStoreCallSite() != null
|
.filter(e -> entry.getStore() != null && e.getDefaultDataStoreCallSite() != null
|
||||||
&& e.getDefaultDataStoreCallSite()
|
&& e.getDefaultDataStoreCallSite()
|
||||||
.getApplicableClass()
|
.getApplicableClass()
|
||||||
.isAssignableFrom(entry.getStore().getClass())
|
.isAssignableFrom(entry.getStore().getClass())
|
||||||
&& e.getDefaultDataStoreCallSite().isApplicable(entry.ref()))
|
&& e.getDefaultDataStoreCallSite().isApplicable(entry.ref()))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.map(ActionProvider::getDefaultDataStoreCallSite)
|
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
this.defaultActionProvider.setValue(defaultProvider);
|
this.defaultActionProvider.setValue(defaultProvider);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
actionProviders
|
var newProviders = ActionProvider.ALL.stream()
|
||||||
.get(dataStoreActionProvider)
|
.filter(dataStoreActionProvider -> {
|
||||||
.set(dataStoreActionProvider
|
return showActionProvider(dataStoreActionProvider);
|
||||||
.getDataStoreCallSite()
|
})
|
||||||
.getApplicableClass()
|
.sorted(Comparator.comparing(
|
||||||
.isAssignableFrom(entry.getStore().getClass())
|
actionProvider -> actionProvider.getLeafDataStoreCallSite() != null &&
|
||||||
&& dataStoreActionProvider
|
actionProvider.getLeafDataStoreCallSite().isSystemAction()))
|
||||||
.getDataStoreCallSite()
|
.toList();
|
||||||
.isApplicable(entry.ref()));
|
if (!actionProviders.equals(newProviders)) {
|
||||||
|
actionProviders.setAll(newProviders);
|
||||||
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
ErrorEvent.fromThrowable(ex).handle();
|
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() {
|
public void refreshChildren() {
|
||||||
|
@ -220,17 +222,31 @@ public class StoreEntryWrapper {
|
||||||
var found = getDefaultActionProvider().getValue();
|
var found = getDefaultActionProvider().getValue();
|
||||||
entry.notifyUpdate(true, false);
|
entry.notifyUpdate(true, false);
|
||||||
if (found != null) {
|
if (found != null) {
|
||||||
found.createAction(entry.ref()).execute();
|
var act = found.getDefaultDataStoreCallSite().createAction(entry.ref());
|
||||||
|
runAction(act,found.getDefaultDataStoreCallSite().showBusy());
|
||||||
} else {
|
} else {
|
||||||
entry.setExpanded(!entry.isExpanded());
|
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() {
|
public void toggleExpanded() {
|
||||||
this.expanded.set(!expanded.getValue());
|
this.expanded.set(!expanded.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean shouldShow(String filter) {
|
public boolean matchesFilter(String filter) {
|
||||||
if (filter == null || nameProperty().getValue().toLowerCase().contains(filter.toLowerCase())) {
|
if (filter == null || nameProperty().getValue().toLowerCase().contains(filter.toLowerCase())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
package io.xpipe.app.comp.store;
|
package io.xpipe.app.comp.store;
|
||||||
|
|
||||||
|
import atlantafx.base.theme.Styles;
|
||||||
import io.xpipe.app.core.AppFont;
|
import io.xpipe.app.core.AppFont;
|
||||||
import io.xpipe.app.core.AppI18n;
|
import io.xpipe.app.core.AppI18n;
|
||||||
import io.xpipe.app.fxcomps.SimpleComp;
|
import io.xpipe.app.fxcomps.SimpleComp;
|
||||||
import io.xpipe.app.fxcomps.impl.PrettySvgComp;
|
import io.xpipe.app.fxcomps.impl.PrettySvgComp;
|
||||||
|
import io.xpipe.app.prefs.AppPrefs;
|
||||||
import io.xpipe.app.storage.DataStorage;
|
import io.xpipe.app.storage.DataStorage;
|
||||||
import io.xpipe.app.util.ScanAlert;
|
import io.xpipe.app.util.ScanAlert;
|
||||||
|
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
|
@ -15,14 +17,11 @@ import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
import atlantafx.base.theme.Styles;
|
|
||||||
import org.kordamp.ikonli.javafx.FontIcon;
|
import org.kordamp.ikonli.javafx.FontIcon;
|
||||||
|
|
||||||
public class StoreIntroComp extends SimpleComp {
|
public class StoreIntroComp extends SimpleComp {
|
||||||
|
|
||||||
@Override
|
private Region createIntro() {
|
||||||
public Region createSimple() {
|
|
||||||
var title = new Label();
|
var title = new Label();
|
||||||
title.textProperty().bind(AppI18n.observable("storeIntroTitle"));
|
title.textProperty().bind(AppI18n.observable("storeIntroTitle"));
|
||||||
title.getStyleClass().add(Styles.TEXT_BOLD);
|
title.getStyleClass().add(Styles.TEXT_BOLD);
|
||||||
|
@ -45,7 +44,7 @@ public class StoreIntroComp extends SimpleComp {
|
||||||
text.setSpacing(5);
|
text.setSpacing(5);
|
||||||
text.setAlignment(Pos.CENTER_LEFT);
|
text.setAlignment(Pos.CENTER_LEFT);
|
||||||
var hbox = new HBox(img, text);
|
var hbox = new HBox(img, text);
|
||||||
hbox.setSpacing(35);
|
hbox.setSpacing(55);
|
||||||
hbox.setAlignment(Pos.CENTER);
|
hbox.setAlignment(Pos.CENTER);
|
||||||
|
|
||||||
var v = new VBox(hbox, scanPane);
|
var v = new VBox(hbox, scanPane);
|
||||||
|
@ -56,8 +55,63 @@ public class StoreIntroComp extends SimpleComp {
|
||||||
|
|
||||||
v.setSpacing(10);
|
v.setSpacing(10);
|
||||||
v.getStyleClass().add("intro");
|
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);
|
var sp = new StackPane(v);
|
||||||
|
sp.setPadding(new Insets(40, 0, 0, 0));
|
||||||
sp.setAlignment(Pos.CENTER);
|
sp.setAlignment(Pos.CENTER);
|
||||||
sp.setPickOnBounds(false);
|
sp.setPickOnBounds(false);
|
||||||
return sp;
|
return sp;
|
||||||
|
|
|
@ -14,9 +14,7 @@ import javafx.beans.value.ObservableValue;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
|
|
||||||
import java.util.Comparator;
|
import java.util.*;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.function.ToIntFunction;
|
import java.util.function.ToIntFunction;
|
||||||
|
|
||||||
|
@ -87,7 +85,7 @@ public class StoreSection {
|
||||||
}
|
}
|
||||||
seen.add(wrapper);
|
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()) {
|
if (found.isPresent()) {
|
||||||
return count(found.get().getWrapper(), seen);
|
return count(found.get().getWrapper(), seen);
|
||||||
} else {
|
} else {
|
||||||
|
@ -125,16 +123,16 @@ public class StoreSection {
|
||||||
category,
|
category,
|
||||||
StoreViewState.get().getEntriesListChangeObservable());
|
StoreViewState.get().getEntriesListChangeObservable());
|
||||||
var cached = topLevel.mapped(
|
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 ordered = sorted(cached, category);
|
||||||
var shown = ordered.filtered(
|
var shown = ordered.filtered(
|
||||||
section -> {
|
section -> {
|
||||||
var showFilter = filterString == null || section.matchesFilter(filterString.get());
|
// matches filter
|
||||||
var matchesSelector = section.anyMatches(entryFilter);
|
return (filterString == null || section.matchesFilter(filterString.get())) &&
|
||||||
var sameCategory = category == null
|
// matches selector
|
||||||
|| category.getValue() == null
|
(section.anyMatches(entryFilter)) &&
|
||||||
|| showInCategory(category.getValue(), section.getWrapper());
|
// same category
|
||||||
return showFilter && matchesSelector && sameCategory;
|
(category == null || category.getValue() == null || showInCategory(category.getValue(), section.getWrapper()));
|
||||||
},
|
},
|
||||||
category,
|
category,
|
||||||
filterString);
|
filterString);
|
||||||
|
@ -142,6 +140,7 @@ public class StoreSection {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static StoreSection create(
|
private static StoreSection create(
|
||||||
|
List<StoreEntryWrapper> parents,
|
||||||
StoreEntryWrapper e,
|
StoreEntryWrapper e,
|
||||||
int depth,
|
int depth,
|
||||||
DerivedObservableList<StoreEntryWrapper> all,
|
DerivedObservableList<StoreEntryWrapper> all,
|
||||||
|
@ -161,31 +160,28 @@ public class StoreSection {
|
||||||
// .map(found -> found.equals(e.getEntry()))
|
// .map(found -> found.equals(e.getEntry()))
|
||||||
// .orElse(false);
|
// .orElse(false);
|
||||||
|
|
||||||
// This check is fast as the children are cached in the storage
|
// is children. This check is fast as the children are cached in the storage
|
||||||
var isChildren = DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry());
|
return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry()) &&
|
||||||
var showProvider = other.getEntry().getProvider() == null ||
|
// show provider
|
||||||
other.getEntry().getProvider().shouldShow(other);
|
(!other.getEntry().getValidity().isUsable() || other.getEntry().getProvider().shouldShow(other));
|
||||||
return isChildren && showProvider;
|
|
||||||
}, e.getPersistentState(), e.getCache(), StoreViewState.get().getEntriesListChangeObservable());
|
}, e.getPersistentState(), e.getCache(), StoreViewState.get().getEntriesListChangeObservable());
|
||||||
var cached = allChildren.mapped(
|
var l = new ArrayList<>(parents);
|
||||||
entry1 -> create(entry1, depth + 1, all, entryFilter, filterString, category));
|
l.add(e);
|
||||||
|
var cached = allChildren.mapped(c -> create(l, c, depth + 1, all, entryFilter, filterString, category));
|
||||||
var ordered = sorted(cached, category);
|
var ordered = sorted(cached, category);
|
||||||
var filtered = ordered.filtered(
|
var filtered = ordered.filtered(
|
||||||
section -> {
|
section -> {
|
||||||
var showFilter = filterString == null || section.matchesFilter(filterString.get());
|
// matches filter
|
||||||
var matchesSelector = section.anyMatches(entryFilter);
|
return (filterString == null || section.matchesFilter(filterString.get()) || l.stream().anyMatch(p -> p.matchesFilter(filterString.get()))) &&
|
||||||
// Prevent updates for children on category switching by checking depth
|
// matches selector
|
||||||
var showCategory = category == null
|
section.anyMatches(entryFilter) &&
|
||||||
|| category.getValue() == null
|
// matches category
|
||||||
|| showInCategory(category.getValue(), section.getWrapper())
|
// Prevent updates for children on category switching by checking depth
|
||||||
|| depth > 0;
|
(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
|
// not root
|
||||||
// again here
|
// If this entry is already shown as root due to a different category than parent, don't show it
|
||||||
var notRoot =
|
// again here
|
||||||
!DataStorage.get().isRootEntry(section.getWrapper().getEntry());
|
!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,
|
category,
|
||||||
filterString,
|
filterString,
|
||||||
|
@ -214,7 +210,7 @@ public class StoreSection {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean matchesFilter(String filter) {
|
public boolean matchesFilter(String filter) {
|
||||||
return anyMatches(storeEntryWrapper -> storeEntryWrapper.shouldShow(filter));
|
return anyMatches(storeEntryWrapper -> storeEntryWrapper.matchesFilter(filter));
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean anyMatches(Predicate<StoreEntryWrapper> c) {
|
public boolean anyMatches(Predicate<StoreEntryWrapper> c) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package io.xpipe.app.comp.store;
|
||||||
|
|
||||||
import io.xpipe.app.core.AppCache;
|
import io.xpipe.app.core.AppCache;
|
||||||
import io.xpipe.app.fxcomps.util.DerivedObservableList;
|
import io.xpipe.app.fxcomps.util.DerivedObservableList;
|
||||||
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
import io.xpipe.app.issue.ErrorEvent;
|
import io.xpipe.app.issue.ErrorEvent;
|
||||||
import io.xpipe.app.prefs.AppPrefs;
|
import io.xpipe.app.prefs.AppPrefs;
|
||||||
import io.xpipe.app.storage.DataStorage;
|
import io.xpipe.app.storage.DataStorage;
|
||||||
|
@ -117,6 +118,18 @@ public class StoreViewState {
|
||||||
.orElseThrow()));
|
.orElseThrow()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void toggleStoreOrderUpdate() {
|
||||||
|
PlatformThread.runLaterIfNeeded(() -> {
|
||||||
|
entriesOrderChangeObservable.set(entriesOrderChangeObservable.get() + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toggleStoreListUpdate() {
|
||||||
|
PlatformThread.runLaterIfNeeded(() -> {
|
||||||
|
entriesListChangeObservable.set(entriesListChangeObservable.get() + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void addListeners() {
|
private void addListeners() {
|
||||||
if (AppPrefs.get() != null) {
|
if (AppPrefs.get() != null) {
|
||||||
AppPrefs.get().condenseConnectionDisplay().addListener((observable, oldValue, newValue) -> {
|
AppPrefs.get().condenseConnectionDisplay().addListener((observable, oldValue, newValue) -> {
|
||||||
|
@ -136,14 +149,14 @@ public class StoreViewState {
|
||||||
@Override
|
@Override
|
||||||
public void onStoreOrderUpdate() {
|
public void onStoreOrderUpdate() {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
entriesOrderChangeObservable.set(entriesOrderChangeObservable.get() + 1);
|
toggleStoreOrderUpdate();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStoreListUpdate() {
|
public void onStoreListUpdate() {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
entriesListChangeObservable.set(entriesListChangeObservable.get() + 1);
|
toggleStoreListUpdate();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,11 +294,9 @@ public class StoreViewState {
|
||||||
public int compare(StoreCategoryWrapper o1, StoreCategoryWrapper o2) {
|
public int compare(StoreCategoryWrapper o1, StoreCategoryWrapper o2) {
|
||||||
var o1Root = o1.getRoot();
|
var o1Root = o1.getRoot();
|
||||||
var o2Root = o2.getRoot();
|
var o2Root = o2.getRoot();
|
||||||
|
|
||||||
if (o1Root.equals(getAllConnectionsCategory()) && !o1Root.equals(o2Root)) {
|
if (o1Root.equals(getAllConnectionsCategory()) && !o1Root.equals(o2Root)) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (o2Root.equals(getAllConnectionsCategory()) && !o1Root.equals(o2Root)) {
|
if (o2Root.equals(getAllConnectionsCategory()) && !o1Root.equals(o2Root)) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
@ -302,6 +313,22 @@ public class StoreViewState {
|
||||||
return 1;
|
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());
|
var parent = compare(o1.getParent(), o2.getParent());
|
||||||
if (parent != 0) {
|
if (parent != 0) {
|
||||||
return parent;
|
return parent;
|
||||||
|
|
|
@ -5,6 +5,7 @@ import io.xpipe.app.core.mode.OperationMode;
|
||||||
import io.xpipe.app.issue.ErrorEvent;
|
import io.xpipe.app.issue.ErrorEvent;
|
||||||
import io.xpipe.app.launcher.LauncherInput;
|
import io.xpipe.app.launcher.LauncherInput;
|
||||||
import io.xpipe.app.prefs.AppPrefs;
|
import io.xpipe.app.prefs.AppPrefs;
|
||||||
|
import io.xpipe.app.util.PlatformState;
|
||||||
import io.xpipe.app.util.ThreadHelper;
|
import io.xpipe.app.util.ThreadHelper;
|
||||||
import io.xpipe.core.process.OsType;
|
import io.xpipe.core.process.OsType;
|
||||||
|
|
||||||
|
@ -16,6 +17,10 @@ import java.util.List;
|
||||||
public class AppDesktopIntegration {
|
public class AppDesktopIntegration {
|
||||||
|
|
||||||
public static void setupDesktopIntegrations() {
|
public static void setupDesktopIntegrations() {
|
||||||
|
if (PlatformState.getCurrent() != PlatformState.RUNNING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (Desktop.isDesktopSupported()) {
|
if (Desktop.isDesktopSupported()) {
|
||||||
Desktop.getDesktop().addAppEventListener(new SystemSleepListener() {
|
Desktop.getDesktop().addAppEventListener(new SystemSleepListener() {
|
||||||
|
|
|
@ -72,7 +72,7 @@ public class AppExtensionManager {
|
||||||
private void loadBaseExtension() {
|
private void loadBaseExtension() {
|
||||||
var baseModule = findAndParseExtension("base", ModuleLayer.boot());
|
var baseModule = findAndParseExtension("base", ModuleLayer.boot());
|
||||||
if (baseModule.isEmpty()) {
|
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();
|
baseLayer = baseModule.get().getModule().getLayer();
|
||||||
|
|
|
@ -50,11 +50,10 @@ public class AppProperties {
|
||||||
Properties props = new Properties();
|
Properties props = new Properties();
|
||||||
props.load(Files.newInputStream(propsFile));
|
props.load(Files.newInputStream(propsFile));
|
||||||
props.forEach((key, value) -> {
|
props.forEach((key, value) -> {
|
||||||
if (System.getProperty(key.toString()) != null) {
|
// Don't overwrite existing properties
|
||||||
return;
|
if (System.getProperty(key.toString()) == null) {
|
||||||
|
System.setProperty(key.toString(), value.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
System.setProperty(key.toString(), value.toString());
|
|
||||||
});
|
});
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
ErrorEvent.fromThrowable(e).handle();
|
ErrorEvent.fromThrowable(e).handle();
|
||||||
|
|
|
@ -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<FailableRunnable<Exception>> post = new AtomicReference<>();
|
|
||||||
var res = prov.get()
|
|
||||||
.handleRequest(
|
|
||||||
new BeaconHandler() {
|
|
||||||
@Override
|
|
||||||
public void postResponse(FailableRunnable<Exception> 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 <T extends ResponseMessage> 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 extends RequestMessage> 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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -235,10 +235,7 @@ public class AppTheme {
|
||||||
.collect(Collectors.joining("\n")));
|
.collect(Collectors.joining("\n")));
|
||||||
});
|
});
|
||||||
|
|
||||||
var out = Files.createTempFile(id, ".css");
|
Application.setUserAgentStylesheet(Styles.toDataURI(builder.toString()));
|
||||||
Files.writeString(out, builder.toString());
|
|
||||||
|
|
||||||
Application.setUserAgentStylesheet(out.toUri().toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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_LIGHT = new Theme("cupertinoLight", "cupertino", new CupertinoLight());
|
||||||
public static final Theme CUPERTINO_DARK = new Theme("cupertinoDark", "cupertino", new CupertinoDark());
|
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 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
|
// Adjust this to create your own theme
|
||||||
public static final Theme CUSTOM = new DerivedTheme("custom", "primer", "Custom", new PrimerDark());
|
public static final Theme CUSTOM = new DerivedTheme("custom", "primer", "Custom", new PrimerDark());
|
||||||
|
|
||||||
// Also include your custom theme here
|
// Also include your custom theme here
|
||||||
public static final List<Theme> ALL =
|
public static final List<Theme> 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;
|
protected final String id;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package io.xpipe.app.core.mode;
|
package io.xpipe.app.core.mode;
|
||||||
|
|
||||||
|
import io.xpipe.app.beacon.AppBeaconServer;
|
||||||
import io.xpipe.app.browser.session.BrowserSessionModel;
|
import io.xpipe.app.browser.session.BrowserSessionModel;
|
||||||
import io.xpipe.app.comp.store.StoreViewState;
|
import io.xpipe.app.comp.store.StoreViewState;
|
||||||
import io.xpipe.app.core.*;
|
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.LicenseProvider;
|
||||||
import io.xpipe.app.util.LocalShell;
|
import io.xpipe.app.util.LocalShell;
|
||||||
import io.xpipe.app.util.UnlockAlert;
|
import io.xpipe.app.util.UnlockAlert;
|
||||||
import io.xpipe.core.util.JacksonMapper;
|
|
||||||
|
|
||||||
public class BaseMode extends OperationMode {
|
public class BaseMode extends OperationMode {
|
||||||
|
|
||||||
|
@ -43,12 +43,8 @@ public class BaseMode extends OperationMode {
|
||||||
// if (true) throw new IllegalStateException();
|
// if (true) throw new IllegalStateException();
|
||||||
|
|
||||||
TrackEvent.info("Initializing base mode components ...");
|
TrackEvent.info("Initializing base mode components ...");
|
||||||
AppExtensionManager.init(true);
|
|
||||||
JacksonMapper.initModularized(AppExtensionManager.getInstance().getExtendedLayer());
|
|
||||||
AppI18n.init();
|
AppI18n.init();
|
||||||
LicenseProvider.get().init();
|
LicenseProvider.get().init();
|
||||||
AppPrefs.initLocal();
|
|
||||||
AppI18n.init();
|
|
||||||
AppCertutilCheck.check();
|
AppCertutilCheck.check();
|
||||||
AppAvCheck.check();
|
AppAvCheck.check();
|
||||||
AppSid.init();
|
AppSid.init();
|
||||||
|
@ -56,8 +52,8 @@ public class BaseMode extends OperationMode {
|
||||||
AppShellCheck.check();
|
AppShellCheck.check();
|
||||||
XPipeDistributionType.init();
|
XPipeDistributionType.init();
|
||||||
AppPrefs.setDefaults();
|
AppPrefs.setDefaults();
|
||||||
// Initialize socket server as we should be prepared for git askpass commands
|
// Initialize beacon server as we should be prepared for git askpass commands
|
||||||
AppSocketServer.init();
|
AppBeaconServer.init();
|
||||||
GitStorageHandler.getInstance().init();
|
GitStorageHandler.getInstance().init();
|
||||||
GitStorageHandler.getInstance().setupRepositoryAndPull();
|
GitStorageHandler.getInstance().setupRepositoryAndPull();
|
||||||
AppPrefs.initSharedRemote();
|
AppPrefs.initSharedRemote();
|
||||||
|
@ -85,8 +81,8 @@ public class BaseMode extends OperationMode {
|
||||||
AppResources.reset();
|
AppResources.reset();
|
||||||
AppExtensionManager.reset();
|
AppExtensionManager.reset();
|
||||||
AppDataLock.unlock();
|
AppDataLock.unlock();
|
||||||
// Shut down socket server last to keep a non-daemon thread running
|
// Shut down server last to keep a non-daemon thread running
|
||||||
AppSocketServer.reset();
|
AppBeaconServer.reset();
|
||||||
TrackEvent.info("Background mode shutdown finished");
|
TrackEvent.info("Background mode shutdown finished");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import io.xpipe.app.core.check.AppTempCheck;
|
||||||
import io.xpipe.app.core.check.AppUserDirectoryCheck;
|
import io.xpipe.app.core.check.AppUserDirectoryCheck;
|
||||||
import io.xpipe.app.issue.*;
|
import io.xpipe.app.issue.*;
|
||||||
import io.xpipe.app.launcher.LauncherCommand;
|
import io.xpipe.app.launcher.LauncherCommand;
|
||||||
|
import io.xpipe.app.prefs.AppPrefs;
|
||||||
import io.xpipe.app.util.LocalShell;
|
import io.xpipe.app.util.LocalShell;
|
||||||
import io.xpipe.app.util.PlatformState;
|
import io.xpipe.app.util.PlatformState;
|
||||||
import io.xpipe.app.util.ThreadHelper;
|
import io.xpipe.app.util.ThreadHelper;
|
||||||
|
@ -109,6 +110,9 @@ public abstract class OperationMode {
|
||||||
AppProperties.logArguments(args);
|
AppProperties.logArguments(args);
|
||||||
AppProperties.logSystemProperties();
|
AppProperties.logSystemProperties();
|
||||||
AppProperties.logPassedProperties();
|
AppProperties.logPassedProperties();
|
||||||
|
AppExtensionManager.init(true);
|
||||||
|
AppI18n.init();
|
||||||
|
AppPrefs.initLocal();
|
||||||
TrackEvent.info("Finished initial setup");
|
TrackEvent.info("Finished initial setup");
|
||||||
} catch (Throwable ex) {
|
} catch (Throwable ex) {
|
||||||
ErrorEvent.fromThrowable(ex).term().handle();
|
ErrorEvent.fromThrowable(ex).term().handle();
|
||||||
|
|
|
@ -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<AskpassExchange.Request, AskpassExchange.Response> {
|
|
||||||
|
|
||||||
@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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<DialogExchange.Request, DialogExchange.Response> {
|
|
||||||
|
|
||||||
private static final Map<UUID, Dialog> openDialogs = new HashMap<>();
|
|
||||||
private static final Map<UUID, FailableConsumer<?, Exception>> openDialogConsumers = new HashMap<>();
|
|
||||||
|
|
||||||
public static <T> DialogReference add(Dialog d, FailableConsumer<T, Exception> onCompletion) throws Exception {
|
|
||||||
return add(d, UUID.randomUUID(), onCompletion);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> DialogReference add(Dialog d, UUID uuid, FailableConsumer<T, Exception> 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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<FocusExchange.Request, FocusExchange.Response> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Response handleRequest(BeaconHandler handler, Request msg) {
|
|
||||||
OperationMode.switchUp(OperationMode.map(msg.getMode()));
|
|
||||||
return Response.builder().build();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<LaunchExchange.Request, LaunchExchange.Response> {
|
|
||||||
|
|
||||||
@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<String> 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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<RQ extends RequestMessage, RS extends ResponseMessage> 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;
|
|
||||||
}
|
|
|
@ -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<MessageExchangeImpl<?, ?>> ALL;
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public static <RQ extends RequestMessage, RS extends ResponseMessage> Optional<MessageExchangeImpl<RQ, RS>> byId(
|
|
||||||
String name) {
|
|
||||||
var r = ALL.stream().filter(d -> d.getId().equals(name)).findAny();
|
|
||||||
return Optional.ofNullable((MessageExchangeImpl<RQ, RS>) r.orElse(null));
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public static <RQ extends RequestMessage, RS extends ResponseMessage>
|
|
||||||
Optional<MessageExchangeImpl<RQ, RS>> byRequest(RQ req) {
|
|
||||||
var r = ALL.stream()
|
|
||||||
.filter(d -> d.getRequestClass().equals(req.getClass()))
|
|
||||||
.findAny();
|
|
||||||
return Optional.ofNullable((MessageExchangeImpl<RQ, RS>) r.orElse(null));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<MessageExchangeImpl<?, ?>> 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());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<OpenExchange.Request, OpenExchange.Response> {
|
|
||||||
|
|
||||||
@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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<QueryStoreExchange.Request, QueryStoreExchange.Response> {
|
|
||||||
|
|
||||||
@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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<TerminalLaunchExchange.Request, TerminalLaunchExchange.Response> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Response handleRequest(BeaconHandler handler, Request msg) throws ClientException {
|
|
||||||
var r = TerminalLauncherManager.performLaunch(msg.getRequest());
|
|
||||||
return Response.builder().targetFile(r).build();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<TerminalWaitExchange.Request, TerminalWaitExchange.Response> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Response handleRequest(BeaconHandler handler, Request msg) throws ServerException, ClientException {
|
|
||||||
TerminalLauncherManager.waitForCompletion(msg.getRequest());
|
|
||||||
return Response.builder().build();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<DrainExchange.Request, DrainExchange.Response> {
|
|
||||||
|
|
||||||
@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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<EditStoreExchange.Request, EditStoreExchange.Response> {
|
|
||||||
|
|
||||||
@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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<ListStoresExchange.Request, ListStoresExchange.Response> {
|
|
||||||
|
|
||||||
@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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<ModeExchange.Request, ModeExchange.Response> {
|
|
||||||
|
|
||||||
@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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<ReadDrainExchange.Request, ReadDrainExchange.Response> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Response handleRequest(BeaconHandler handler, Request msg) {
|
|
||||||
return ReadDrainExchange.Response.builder().build();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<RemoveStoreExchange.Request, RemoveStoreExchange.Response> {
|
|
||||||
|
|
||||||
@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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<RenameStoreExchange.Request, RenameStoreExchange.Response> {
|
|
||||||
|
|
||||||
@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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<SinkExchange.Request, SinkExchange.Response> {
|
|
||||||
|
|
||||||
@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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<StatusExchange.Request, StatusExchange.Response> {
|
|
||||||
|
|
||||||
@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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<StopExchange.Request, StopExchange.Response> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Response handleRequest(BeaconHandler handler, Request msg) {
|
|
||||||
handler.postResponse(() -> {
|
|
||||||
ThreadHelper.runAsync(() -> {
|
|
||||||
ThreadHelper.sleep(1000);
|
|
||||||
OperationMode.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return Response.builder().success(true).build();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<StoreAddExchange.Request, StoreAddExchange.Response> {
|
|
||||||
|
|
||||||
@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<Dialog>();
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<StoreProviderListExchange.Request, StoreProviderListExchange.Response> {
|
|
||||||
|
|
||||||
@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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -33,10 +33,6 @@ public interface ActionProvider {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
default boolean isActive() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
default String getProFeatureId() {
|
default String getProFeatureId() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -45,7 +41,12 @@ public interface ActionProvider {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
default DataStoreCallSite<?> getDataStoreCallSite() {
|
default LeafDataStoreCallSite<?> getLeafDataStoreCallSite() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
default BranchDataStoreCallSite<?> getBranchDataStoreCallSite() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,8 +56,6 @@ public interface ActionProvider {
|
||||||
|
|
||||||
interface Action {
|
interface Action {
|
||||||
|
|
||||||
boolean requiresJavaFXPlatform();
|
|
||||||
|
|
||||||
void execute() throws Exception;
|
void execute() throws Exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,6 +88,10 @@ public interface ActionProvider {
|
||||||
default boolean isApplicable(DataStoreEntryRef<T> o) {
|
default boolean isApplicable(DataStoreEntryRef<T> o) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default boolean showBusy() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DataStoreCallSite<T extends DataStore> {
|
interface DataStoreCallSite<T extends DataStore> {
|
||||||
|
@ -97,14 +100,6 @@ public interface ActionProvider {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
default boolean canLinkTo() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Action createAction(DataStoreEntryRef<T> store);
|
|
||||||
|
|
||||||
Class<T> getApplicableClass();
|
|
||||||
|
|
||||||
default boolean isMajor(DataStoreEntryRef<T> o) {
|
default boolean isMajor(DataStoreEntryRef<T> o) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -117,14 +112,30 @@ public interface ActionProvider {
|
||||||
|
|
||||||
String getIcon(DataStoreEntryRef<T> store);
|
String getIcon(DataStoreEntryRef<T> store);
|
||||||
|
|
||||||
default ActiveType activeType() {
|
Class<T> getApplicableClass();
|
||||||
return ActiveType.ONLY_SHOW_IF_ENABLED;
|
|
||||||
|
default boolean showBusy() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BranchDataStoreCallSite<T extends DataStore> extends DataStoreCallSite<T> {
|
||||||
|
|
||||||
|
default List<ActionProvider> getChildren() {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LeafDataStoreCallSite<T extends DataStore> extends DataStoreCallSite<T> {
|
||||||
|
|
||||||
|
default boolean canLinkTo() {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ActiveType {
|
Action createAction(DataStoreEntryRef<T> store);
|
||||||
ONLY_SHOW_IF_ENABLED,
|
|
||||||
ALWAYS_SHOW,
|
default boolean requiresValidStore() {
|
||||||
ALWAYS_ENABLE
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,14 +145,6 @@ public interface ActionProvider {
|
||||||
public void init(ModuleLayer layer) {
|
public void init(ModuleLayer layer) {
|
||||||
ALL.addAll(ServiceLoader.load(layer, ActionProvider.class).stream()
|
ALL.addAll(ServiceLoader.load(layer, ActionProvider.class).stream()
|
||||||
.map(actionProviderProvider -> actionProviderProvider.get())
|
.map(actionProviderProvider -> actionProviderProvider.get())
|
||||||
.filter(provider -> {
|
|
||||||
try {
|
|
||||||
return provider.isActive();
|
|
||||||
} catch (Throwable e) {
|
|
||||||
ErrorEvent.fromThrowable(e).handle();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.toList());
|
.toList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<DataStorageExtensionProvider> ALL;
|
||||||
|
|
||||||
|
public static List<DataStorageExtensionProvider> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,10 +12,8 @@ import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.issue.ErrorEvent;
|
import io.xpipe.app.issue.ErrorEvent;
|
||||||
import io.xpipe.app.storage.DataStorage;
|
import io.xpipe.app.storage.DataStorage;
|
||||||
import io.xpipe.app.storage.DataStoreEntry;
|
import io.xpipe.app.storage.DataStoreEntry;
|
||||||
import io.xpipe.core.dialog.Dialog;
|
|
||||||
import io.xpipe.core.store.DataStore;
|
import io.xpipe.core.store.DataStore;
|
||||||
import io.xpipe.core.util.JacksonizedValue;
|
import io.xpipe.core.util.JacksonizedValue;
|
||||||
|
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.Property;
|
import javafx.beans.property.Property;
|
||||||
|
@ -31,7 +29,9 @@ public interface DataStoreProvider {
|
||||||
default boolean shouldShow(StoreEntryWrapper w) {
|
default boolean shouldShow(StoreEntryWrapper w) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default void onChildrenRefresh(DataStoreEntry entry) {}
|
||||||
|
|
||||||
default ObservableBooleanValue busy(StoreEntryWrapper wrapper) {
|
default ObservableBooleanValue busy(StoreEntryWrapper wrapper) {
|
||||||
return new SimpleBooleanProperty(false);
|
return new SimpleBooleanProperty(false);
|
||||||
}
|
}
|
||||||
|
@ -196,10 +196,6 @@ public interface DataStoreProvider {
|
||||||
return getModuleName() + ":" + getId() + "_icon.svg";
|
return getModuleName() + ":" + getId() + "_icon.svg";
|
||||||
}
|
}
|
||||||
|
|
||||||
default Dialog dialogForStore(DataStore store) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
default DataStore defaultStore() {
|
default DataStore defaultStore() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -216,6 +212,7 @@ public interface DataStoreProvider {
|
||||||
HOST,
|
HOST,
|
||||||
DATABASE,
|
DATABASE,
|
||||||
SHELL,
|
SHELL,
|
||||||
|
SERVICE,
|
||||||
COMMAND,
|
COMMAND,
|
||||||
TUNNEL,
|
TUNNEL,
|
||||||
SCRIPT,
|
SCRIPT,
|
||||||
|
|
|
@ -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.<StatefulDataStore<EnabledStoreState>>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);
|
||||||
|
}
|
||||||
|
}
|
25
app/src/main/java/io/xpipe/app/ext/EnabledStoreProvider.java
Normal file
25
app/src/main/java/io/xpipe/app/ext/EnabledStoreProvider.java
Normal file
|
@ -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.<StatefulDataStore<EnabledStoreState>>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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,6 @@ public class ExtensionException extends RuntimeException {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ExtensionException corrupt(String message) {
|
public static ExtensionException corrupt(String message) {
|
||||||
return new ExtensionException(message + ". Is the installation corrupt?");
|
return new ExtensionException(message + ". Is the installation data corrupt?");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,14 @@ import io.xpipe.app.comp.store.StoreEntryComp;
|
||||||
import io.xpipe.app.comp.store.StoreEntryWrapper;
|
import io.xpipe.app.comp.store.StoreEntryWrapper;
|
||||||
import io.xpipe.app.comp.store.StoreSection;
|
import io.xpipe.app.comp.store.StoreSection;
|
||||||
import io.xpipe.app.fxcomps.Comp;
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
|
import io.xpipe.app.fxcomps.util.LabelGraphic;
|
||||||
import io.xpipe.app.util.ThreadHelper;
|
import io.xpipe.app.util.ThreadHelper;
|
||||||
import io.xpipe.core.store.SingletonSessionStore;
|
import io.xpipe.core.store.SingletonSessionStore;
|
||||||
|
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.beans.value.ObservableBooleanValue;
|
import javafx.beans.value.ObservableBooleanValue;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
|
||||||
public interface SingletonSessionStoreProvider extends DataStoreProvider {
|
public interface SingletonSessionStoreProvider extends DataStoreProvider {
|
||||||
|
|
||||||
|
@ -38,7 +40,9 @@ public interface SingletonSessionStoreProvider extends DataStoreProvider {
|
||||||
enabled.set(s.isSessionEnabled());
|
enabled.set(s.isSessionEnabled());
|
||||||
});
|
});
|
||||||
|
|
||||||
var t = new StoreToggleComp(null, sec, enabled, aBoolean -> {
|
ObservableValue<LabelGraphic> 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();
|
SingletonSessionStore<?> s = sec.getWrapper().getEntry().getStore().asNeeded();
|
||||||
if (s.isSessionEnabled() != aBoolean) {
|
if (s.isSessionEnabled() != aBoolean) {
|
||||||
ThreadHelper.runFailableAsync(() -> {
|
ThreadHelper.runFailableAsync(() -> {
|
||||||
|
@ -50,6 +54,7 @@ public interface SingletonSessionStoreProvider extends DataStoreProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
t.tooltipKey("enabled");
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,12 +53,12 @@ public class IconButtonComp extends Comp<CompStructure<Button>> {
|
||||||
});
|
});
|
||||||
// fi.iconColorProperty().bind(button.textFillProperty());
|
// fi.iconColorProperty().bind(button.textFillProperty());
|
||||||
button.setGraphic(fi);
|
button.setGraphic(fi);
|
||||||
button.setOnAction(e -> {
|
if (listener != null) {
|
||||||
if (listener != null) {
|
button.setOnAction(e -> {
|
||||||
listener.run();
|
listener.run();
|
||||||
e.consume();
|
e.consume();
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
button.getStyleClass().add("icon-button-comp");
|
button.getStyleClass().add("icon-button-comp");
|
||||||
return new SimpleCompStructure<>(button);
|
return new SimpleCompStructure<>(button);
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,7 +79,7 @@ public class StoreCategoryComp extends SimpleComp {
|
||||||
}));
|
}));
|
||||||
var shownList = new DerivedObservableList<>(category.getContainedEntries(), true).filtered(
|
var shownList = new DerivedObservableList<>(category.getContainedEntries(), true).filtered(
|
||||||
storeEntryWrapper -> {
|
storeEntryWrapper -> {
|
||||||
return storeEntryWrapper.shouldShow(
|
return storeEntryWrapper.matchesFilter(
|
||||||
StoreViewState.get().getFilterString().getValue());
|
StoreViewState.get().getFilterString().getValue());
|
||||||
},
|
},
|
||||||
StoreViewState.get().getFilterString()).getList();
|
StoreViewState.get().getFilterString()).getList();
|
||||||
|
|
|
@ -129,9 +129,17 @@ public class DerivedObservableList<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public <V> DerivedObservableList<V> mapped(Function<T, V> map) {
|
public <V> DerivedObservableList<V> mapped(Function<T, V> map) {
|
||||||
|
var cache = new HashMap<T, V>();
|
||||||
var l1 = this.<V>createNewDerived();
|
var l1 = this.<V>createNewDerived();
|
||||||
Runnable runnable = () -> {
|
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();
|
runnable.run();
|
||||||
list.addListener((ListChangeListener<? super T>) c -> {
|
list.addListener((ListChangeListener<? super T>) c -> {
|
||||||
|
|
|
@ -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<LabelGraphic> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package io.xpipe.app.launcher;
|
package io.xpipe.app.launcher;
|
||||||
|
|
||||||
|
import io.xpipe.app.beacon.AppBeaconServer;
|
||||||
import io.xpipe.app.core.AppDataLock;
|
import io.xpipe.app.core.AppDataLock;
|
||||||
import io.xpipe.app.core.AppLogs;
|
import io.xpipe.app.core.AppLogs;
|
||||||
import io.xpipe.app.core.AppProperties;
|
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.issue.TrackEvent;
|
||||||
import io.xpipe.app.prefs.AppPrefs;
|
import io.xpipe.app.prefs.AppPrefs;
|
||||||
import io.xpipe.app.util.ThreadHelper;
|
import io.xpipe.app.util.ThreadHelper;
|
||||||
import io.xpipe.beacon.BeaconServer;
|
import io.xpipe.beacon.BeaconClient;
|
||||||
import io.xpipe.beacon.exchange.FocusExchange;
|
import io.xpipe.beacon.BeaconClientInformation;
|
||||||
import io.xpipe.beacon.exchange.OpenExchange;
|
import io.xpipe.beacon.api.DaemonFocusExchange;
|
||||||
|
import io.xpipe.beacon.api.DaemonOpenExchange;
|
||||||
import io.xpipe.core.process.OsType;
|
import io.xpipe.core.process.OsType;
|
||||||
import io.xpipe.core.util.XPipeDaemonMode;
|
import io.xpipe.core.util.XPipeDaemonMode;
|
||||||
import io.xpipe.core.util.XPipeInstallation;
|
import io.xpipe.core.util.XPipeInstallation;
|
||||||
|
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import picocli.CommandLine;
|
import picocli.CommandLine;
|
||||||
|
|
||||||
|
@ -81,26 +82,25 @@ public class LauncherCommand implements Callable<Integer> {
|
||||||
|
|
||||||
private void checkStart() {
|
private void checkStart() {
|
||||||
try {
|
try {
|
||||||
if (BeaconServer.isReachable()) {
|
var port = AppBeaconServer.get().getPort();
|
||||||
try (var con = new LauncherConnection()) {
|
var client = BeaconClient.tryEstablishConnection(port, BeaconClientInformation.Daemon.builder().build());
|
||||||
con.constructSocket();
|
if (client.isPresent()) {
|
||||||
con.performSimpleExchange(FocusExchange.Request.builder()
|
client.get().performRequest(DaemonFocusExchange.Request.builder().mode(getEffectiveMode()).build());
|
||||||
.mode(getEffectiveMode())
|
|
||||||
.build());
|
|
||||||
if (!inputs.isEmpty()) {
|
if (!inputs.isEmpty()) {
|
||||||
con.performSimpleExchange(
|
client.get().performRequest(
|
||||||
OpenExchange.Request.builder().arguments(inputs).build());
|
DaemonOpenExchange.Request.builder().arguments(inputs).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (OsType.getLocal().equals(OsType.MACOS)) {
|
if (OsType.getLocal().equals(OsType.MACOS)) {
|
||||||
Desktop.getDesktop().setOpenURIHandler(e -> {
|
Desktop.getDesktop().setOpenURIHandler(e -> {
|
||||||
con.performSimpleExchange(OpenExchange.Request.builder()
|
try {
|
||||||
.arguments(List.of(e.getURI().toString()))
|
client.get().performRequest(DaemonOpenExchange.Request.builder().arguments(List.of(e.getURI().toString())).build());
|
||||||
.build());
|
} catch (Exception ex) {
|
||||||
|
ErrorEvent.fromThrowable(ex).expected().omit().handle();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
ThreadHelper.sleep(1000);
|
ThreadHelper.sleep(1000);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
TrackEvent.info("Another instance is already running on this port. Quitting ...");
|
TrackEvent.info("Another instance is already running on this port. Quitting ...");
|
||||||
OperationMode.halt(1);
|
OperationMode.halt(1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,7 +2,6 @@ package io.xpipe.app.launcher;
|
||||||
|
|
||||||
import io.xpipe.app.browser.session.BrowserSessionModel;
|
import io.xpipe.app.browser.session.BrowserSessionModel;
|
||||||
import io.xpipe.app.core.AppLayoutModel;
|
import io.xpipe.app.core.AppLayoutModel;
|
||||||
import io.xpipe.app.core.mode.OperationMode;
|
|
||||||
import io.xpipe.app.ext.ActionProvider;
|
import io.xpipe.app.ext.ActionProvider;
|
||||||
import io.xpipe.app.issue.ErrorEvent;
|
import io.xpipe.app.issue.ErrorEvent;
|
||||||
import io.xpipe.app.issue.TrackEvent;
|
import io.xpipe.app.issue.TrackEvent;
|
||||||
|
@ -34,17 +33,13 @@ public abstract class LauncherInput {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var requiresPlatform = all.stream().anyMatch(launcherInput -> launcherInput.requiresJavaFXPlatform());
|
// var requiresPlatform = all.stream().anyMatch(launcherInput -> launcherInput.requiresJavaFXPlatform());
|
||||||
if (requiresPlatform) {
|
// if (requiresPlatform) {
|
||||||
OperationMode.switchToSyncIfPossible(OperationMode.GUI);
|
// OperationMode.switchToSyncIfPossible(OperationMode.GUI);
|
||||||
}
|
// }
|
||||||
var hasGui = OperationMode.get() == OperationMode.GUI;
|
// var hasGui = OperationMode.get() == OperationMode.GUI;
|
||||||
|
|
||||||
all.forEach(launcherInput -> {
|
all.forEach(launcherInput -> {
|
||||||
if (!hasGui && launcherInput.requiresJavaFXPlatform()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
launcherInput.execute();
|
launcherInput.execute();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
@ -102,11 +97,6 @@ public abstract class LauncherInput {
|
||||||
|
|
||||||
Path file;
|
Path file;
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean requiresJavaFXPlatform() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute() {
|
public void execute() {
|
||||||
if (!Files.exists(file)) {
|
if (!Files.exists(file)) {
|
||||||
|
|
|
@ -14,14 +14,13 @@ import io.xpipe.app.terminal.ExternalTerminalType;
|
||||||
import io.xpipe.app.util.PasswordLockSecretValue;
|
import io.xpipe.app.util.PasswordLockSecretValue;
|
||||||
import io.xpipe.core.util.InPlaceSecretValue;
|
import io.xpipe.core.util.InPlaceSecretValue;
|
||||||
import io.xpipe.core.util.ModuleHelper;
|
import io.xpipe.core.util.ModuleHelper;
|
||||||
|
import io.xpipe.core.util.XPipeInstallation;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.property.*;
|
import javafx.beans.property.*;
|
||||||
import javafx.beans.value.ObservableBooleanValue;
|
import javafx.beans.value.ObservableBooleanValue;
|
||||||
import javafx.beans.value.ObservableDoubleValue;
|
import javafx.beans.value.ObservableDoubleValue;
|
||||||
import javafx.beans.value.ObservableStringValue;
|
import javafx.beans.value.ObservableStringValue;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
|
|
||||||
|
@ -121,6 +120,25 @@ public class AppPrefs {
|
||||||
private final StringProperty lockCrypt =
|
private final StringProperty lockCrypt =
|
||||||
mapVaultSpecific(new SimpleStringProperty(), "workspaceLock", String.class);
|
mapVaultSpecific(new SimpleStringProperty(), "workspaceLock", String.class);
|
||||||
|
|
||||||
|
final Property<Integer> 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<Integer> httpServerPort() {
|
||||||
|
return httpServerPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableStringValue apiKey() {
|
||||||
|
return apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableBooleanValue disableApiAuthentication() {
|
||||||
|
return disableApiAuthentication;
|
||||||
|
}
|
||||||
|
|
||||||
private final IntegerProperty editorReloadTimeout =
|
private final IntegerProperty editorReloadTimeout =
|
||||||
map(new SimpleIntegerProperty(1000), "editorReloadTimeout", Integer.class);
|
map(new SimpleIntegerProperty(1000), "editorReloadTimeout", Integer.class);
|
||||||
private final BooleanProperty confirmDeletions =
|
private final BooleanProperty confirmDeletions =
|
||||||
|
@ -153,6 +171,7 @@ public class AppPrefs {
|
||||||
new SshCategory(),
|
new SshCategory(),
|
||||||
new LocalShellCategory(),
|
new LocalShellCategory(),
|
||||||
new SecurityCategory(),
|
new SecurityCategory(),
|
||||||
|
new HttpApiCategory(),
|
||||||
new TroubleshootCategory(),
|
new TroubleshootCategory(),
|
||||||
new DeveloperCategory())
|
new DeveloperCategory())
|
||||||
.filter(appPrefsCategory -> appPrefsCategory.show())
|
.filter(appPrefsCategory -> appPrefsCategory.show())
|
||||||
|
@ -435,12 +454,8 @@ public class AppPrefs {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initDefaultValues() {
|
public void initDefaultValues() {
|
||||||
if (externalEditor.get() == null) {
|
externalEditor.setValue(ExternalEditorType.detectDefault(externalEditor.get()));
|
||||||
ExternalEditorType.detectDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
terminalType.set(ExternalTerminalType.determineDefault(terminalType.get()));
|
terminalType.set(ExternalTerminalType.determineDefault(terminalType.get()));
|
||||||
|
|
||||||
if (rdpClientType.get() == null) {
|
if (rdpClientType.get() == null) {
|
||||||
rdpClientType.setValue(ExternalRdpClientType.determineDefault());
|
rdpClientType.setValue(ExternalRdpClientType.determineDefault());
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,16 +45,17 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue {
|
||||||
@Override
|
@Override
|
||||||
public boolean isAvailable() {
|
public boolean isAvailable() {
|
||||||
try (ShellControl pc = LocalShell.getShell().start()) {
|
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",
|
"mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications",
|
||||||
applicationName))
|
applicationName))
|
||||||
.executeAndCheck();
|
.readStdoutIfPossible();
|
||||||
|
return out.isPresent() && !out.get().isBlank();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
ErrorEvent.fromThrowable(e).handle();
|
ErrorEvent.fromThrowable(e).handle();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isSelectable() {
|
public boolean isSelectable() {
|
||||||
return OsType.getLocal().equals(OsType.MACOS);
|
return OsType.getLocal().equals(OsType.MACOS);
|
||||||
|
|
|
@ -145,43 +145,34 @@ public interface ExternalEditorType extends PrefsChoiceValue {
|
||||||
})
|
})
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
static void detectDefault() {
|
static ExternalEditorType detectDefault(ExternalEditorType existing) {
|
||||||
var typeProperty = AppPrefs.get().externalEditor;
|
// Verify that our selection is still valid
|
||||||
var customProperty = AppPrefs.get().customEditorCommand;
|
if (existing != null && existing.isAvailable()) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
if (OsType.getLocal().equals(OsType.WINDOWS)) {
|
if (OsType.getLocal().equals(OsType.WINDOWS)) {
|
||||||
typeProperty.set(WINDOWS_EDITORS.stream()
|
return WINDOWS_EDITORS.stream()
|
||||||
.filter(PrefsChoiceValue::isAvailable)
|
.filter(PrefsChoiceValue::isAvailable)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse(null));
|
.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (OsType.getLocal().equals(OsType.LINUX)) {
|
if (OsType.getLocal().equals(OsType.LINUX)) {
|
||||||
var env = System.getenv("VISUAL");
|
return LINUX_EDITORS.stream()
|
||||||
if (env != null) {
|
.filter(ExternalApplicationType.PathApplication::isAvailable)
|
||||||
var found = LINUX_EDITORS.stream()
|
.findFirst()
|
||||||
.filter(externalEditorType -> externalEditorType.executable.equalsIgnoreCase(env))
|
.orElse(null);
|
||||||
.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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (OsType.getLocal().equals(OsType.MACOS)) {
|
if (OsType.getLocal().equals(OsType.MACOS)) {
|
||||||
typeProperty.set(MACOS_EDITORS.stream()
|
return MACOS_EDITORS.stream()
|
||||||
.filter(PrefsChoiceValue::isAvailable)
|
.filter(PrefsChoiceValue::isAvailable)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse(null));
|
.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
void launch(Path file) throws Exception;
|
void launch(Path file) throws Exception;
|
||||||
|
@ -194,10 +185,10 @@ public interface ExternalEditorType extends PrefsChoiceValue {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void launch(Path file) throws Exception {
|
public void launch(Path file) throws Exception {
|
||||||
ExternalApplicationHelper.startAsync(CommandBuilder.of()
|
try (var sc = LocalShell.getShell().start()) {
|
||||||
.add("open", "-a")
|
sc.executeSimpleCommand(CommandBuilder.of()
|
||||||
.addQuoted(applicationName)
|
.add("open", "-a").addQuoted(applicationName).addFile(file.toString()));
|
||||||
.addFile(file.toString()));
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
30
app/src/main/java/io/xpipe/app/prefs/HttpApiCategory.java
Normal file
30
app/src/main/java/io/xpipe/app/prefs/HttpApiCategory.java
Normal file
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package io.xpipe.app.prefs;
|
||||||
|
|
||||||
import io.xpipe.app.comp.base.ButtonComp;
|
import io.xpipe.app.comp.base.ButtonComp;
|
||||||
import io.xpipe.app.core.AppI18n;
|
import io.xpipe.app.core.AppI18n;
|
||||||
|
import io.xpipe.app.core.AppProperties;
|
||||||
import io.xpipe.app.fxcomps.Comp;
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.storage.DataStorage;
|
import io.xpipe.app.storage.DataStorage;
|
||||||
import io.xpipe.app.util.DesktopHelper;
|
import io.xpipe.app.util.DesktopHelper;
|
||||||
|
@ -19,8 +20,10 @@ public class SyncCategory extends AppPrefsCategory {
|
||||||
var builder = new OptionsBuilder();
|
var builder = new OptionsBuilder();
|
||||||
builder.addTitle("sync")
|
builder.addTitle("sync")
|
||||||
.sub(new OptionsBuilder()
|
.sub(new OptionsBuilder()
|
||||||
.nameAndDescription("enableGitStorage")
|
.name("enableGitStorage")
|
||||||
|
.description(AppProperties.get().isStaging() ? "enableGitStoragePtbDisabled" : "enableGitStorage")
|
||||||
.addToggle(prefs.enableGitStorage)
|
.addToggle(prefs.enableGitStorage)
|
||||||
|
.disable(AppProperties.get().isStaging())
|
||||||
.nameAndDescription("storageGitRemote")
|
.nameAndDescription("storageGitRemote")
|
||||||
.addString(prefs.storageGitRemote, true)
|
.addString(prefs.storageGitRemote, true)
|
||||||
.disable(prefs.enableGitStorage.not())
|
.disable(prefs.enableGitStorage.not())
|
||||||
|
|
|
@ -5,11 +5,12 @@ import io.xpipe.app.issue.ErrorEvent;
|
||||||
import io.xpipe.app.prefs.AppPrefs;
|
import io.xpipe.app.prefs.AppPrefs;
|
||||||
import io.xpipe.app.util.FixedHierarchyStore;
|
import io.xpipe.app.util.FixedHierarchyStore;
|
||||||
import io.xpipe.app.util.ThreadHelper;
|
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 io.xpipe.core.util.UuidHelper;
|
||||||
|
|
||||||
import javafx.util.Pair;
|
import javafx.util.Pair;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
@ -20,7 +21,6 @@ import java.time.Instant;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
@ -61,6 +61,8 @@ public abstract class DataStorage {
|
||||||
@Setter
|
@Setter
|
||||||
protected DataStoreCategory selectedCategory;
|
protected DataStoreCategory selectedCategory;
|
||||||
|
|
||||||
|
private final Map<DataStore, DataStoreEntry> storeEntryMapCache = Collections.synchronizedMap(new IdentityHashMap<>());
|
||||||
|
|
||||||
public DataStorage() {
|
public DataStorage() {
|
||||||
var prefsDir = AppPrefs.get().storageDirectory().getValue();
|
var prefsDir = AppPrefs.get().storageDirectory().getValue();
|
||||||
this.dir = !Files.exists(prefsDir) || !Files.isDirectory(prefsDir) ? AppPrefs.DEFAULT_STORAGE_DIR : prefsDir;
|
this.dir = !Files.exists(prefsDir) || !Files.isDirectory(prefsDir) ? AppPrefs.DEFAULT_STORAGE_DIR : prefsDir;
|
||||||
|
@ -260,16 +262,10 @@ public abstract class DataStorage {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void refreshValidities(boolean makeValid) {
|
protected void refreshEntries() {
|
||||||
var changed = new AtomicBoolean(false);
|
storeEntries.keySet().forEach(dataStoreEntry -> {
|
||||||
do {
|
dataStoreEntry.refreshStore();
|
||||||
changed.set(false);
|
});
|
||||||
storeEntries.keySet().forEach(dataStoreEntry -> {
|
|
||||||
if (makeValid ? dataStoreEntry.tryMakeValid() : dataStoreEntry.tryMakeInvalid()) {
|
|
||||||
changed.set(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} while (changed.get());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateEntry(DataStoreEntry entry, DataStoreEntry newEntry) {
|
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);
|
var toAdd = Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new);
|
||||||
listeners.forEach(storageListener -> storageListener.onStoreAdd(toAdd));
|
listeners.forEach(storageListener -> storageListener.onStoreAdd(toAdd));
|
||||||
}
|
}
|
||||||
refreshValidities(true);
|
refreshEntries();
|
||||||
|
|
||||||
saveAsync();
|
saveAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -345,21 +340,21 @@ public abstract class DataStorage {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
e.setInRefresh(true);
|
e.incrementBusyCounter();
|
||||||
List<? extends DataStoreEntryRef<? extends FixedChildStore>> newChildren;
|
List<? extends DataStoreEntryRef<? extends FixedChildStore>> newChildren;
|
||||||
try {
|
try {
|
||||||
newChildren = ((FixedHierarchyStore) (e.getStore())).listChildren(e);
|
newChildren = ((FixedHierarchyStore) (e.getStore())).listChildren(e);
|
||||||
e.setInRefresh(false);
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
e.setInRefresh(false);
|
|
||||||
ErrorEvent.fromThrowable(ex).handle();
|
ErrorEvent.fromThrowable(ex).handle();
|
||||||
return false;
|
return false;
|
||||||
|
} finally {
|
||||||
|
e.decrementBusyCounter();
|
||||||
}
|
}
|
||||||
|
|
||||||
var oldChildren = getStoreEntries().stream()
|
|
||||||
.filter(other -> e.equals(getDefaultDisplayParent(other).orElse(null)))
|
var oldChildren = getStoreChildren(e);
|
||||||
.toList();
|
|
||||||
var toRemove = oldChildren.stream()
|
var toRemove = oldChildren.stream()
|
||||||
|
.filter(oc -> oc.getStore() instanceof FixedChildStore)
|
||||||
.filter(oc -> {
|
.filter(oc -> {
|
||||||
var oid = ((FixedChildStore) oc.getStore()).getFixedId();
|
var oid = ((FixedChildStore) oc.getStore()).getFixedId();
|
||||||
if (oid.isEmpty()) {
|
if (oid.isEmpty()) {
|
||||||
|
@ -375,6 +370,10 @@ public abstract class DataStorage {
|
||||||
.toList();
|
.toList();
|
||||||
var toAdd = newChildren.stream()
|
var toAdd = newChildren.stream()
|
||||||
.filter(nc -> {
|
.filter(nc -> {
|
||||||
|
if (nc.getStore() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var nid = nc.getStore().getFixedId();
|
var nid = nc.getStore().getFixedId();
|
||||||
// These can't be automatically generated
|
// These can't be automatically generated
|
||||||
if (nid.isEmpty()) {
|
if (nid.isEmpty()) {
|
||||||
|
@ -382,6 +381,7 @@ public abstract class DataStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
return oldChildren.stream()
|
return oldChildren.stream()
|
||||||
|
.filter(oc -> oc.getStore() instanceof FixedChildStore)
|
||||||
.filter(oc -> ((FixedChildStore) oc.getStore())
|
.filter(oc -> ((FixedChildStore) oc.getStore())
|
||||||
.getFixedId()
|
.getFixedId()
|
||||||
.isPresent())
|
.isPresent())
|
||||||
|
@ -394,6 +394,7 @@ public abstract class DataStorage {
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
var toUpdate = oldChildren.stream()
|
var toUpdate = oldChildren.stream()
|
||||||
|
.filter(oc -> oc.getStore() instanceof FixedChildStore)
|
||||||
.map(oc -> {
|
.map(oc -> {
|
||||||
var oid = ((FixedChildStore) oc.getStore()).getFixedId();
|
var oid = ((FixedChildStore) oc.getStore()).getFixedId();
|
||||||
if (oid.isEmpty()) {
|
if (oid.isEmpty()) {
|
||||||
|
@ -445,7 +446,10 @@ public abstract class DataStorage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
refreshEntries();
|
||||||
saveAsync();
|
saveAsync();
|
||||||
|
toAdd.forEach(dataStoreEntryRef -> dataStoreEntryRef.get().getProvider().onChildrenRefresh(dataStoreEntryRef.getEntry()));
|
||||||
|
toUpdate.forEach(dataStoreEntryRef -> dataStoreEntryRef.getKey().getProvider().onChildrenRefresh(dataStoreEntryRef.getKey()));
|
||||||
return !newChildren.isEmpty();
|
return !newChildren.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -458,7 +462,7 @@ public abstract class DataStorage {
|
||||||
c.forEach(entry -> entry.finalizeEntry());
|
c.forEach(entry -> entry.finalizeEntry());
|
||||||
this.storeEntriesSet.removeAll(c);
|
this.storeEntriesSet.removeAll(c);
|
||||||
this.listeners.forEach(l -> l.onStoreRemove(c.toArray(DataStoreEntry[]::new)));
|
this.listeners.forEach(l -> l.onStoreRemove(c.toArray(DataStoreEntry[]::new)));
|
||||||
refreshValidities(false);
|
refreshEntries();
|
||||||
saveAsync();
|
saveAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -477,7 +481,7 @@ public abstract class DataStorage {
|
||||||
toDelete.forEach(entry -> entry.finalizeEntry());
|
toDelete.forEach(entry -> entry.finalizeEntry());
|
||||||
toDelete.forEach(this.storeEntriesSet::remove);
|
toDelete.forEach(this.storeEntriesSet::remove);
|
||||||
this.listeners.forEach(l -> l.onStoreRemove(toDelete.toArray(DataStoreEntry[]::new)));
|
this.listeners.forEach(l -> l.onStoreRemove(toDelete.toArray(DataStoreEntry[]::new)));
|
||||||
refreshValidities(false);
|
refreshEntries();
|
||||||
saveAsync();
|
saveAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -532,7 +536,7 @@ public abstract class DataStorage {
|
||||||
|
|
||||||
this.listeners.forEach(l -> l.onStoreAdd(e));
|
this.listeners.forEach(l -> l.onStoreAdd(e));
|
||||||
e.initializeEntry();
|
e.initializeEntry();
|
||||||
refreshValidities(true);
|
e.refreshStore();
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -566,11 +570,13 @@ public abstract class DataStorage {
|
||||||
p.setChildrenCache(null);
|
p.setChildrenCache(null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
for (DataStoreEntry e : toAdd) {
|
||||||
|
e.refreshStore();
|
||||||
|
}
|
||||||
this.listeners.forEach(l -> l.onStoreAdd(toAdd.toArray(DataStoreEntry[]::new)));
|
this.listeners.forEach(l -> l.onStoreAdd(toAdd.toArray(DataStoreEntry[]::new)));
|
||||||
for (DataStoreEntry e : toAdd) {
|
for (DataStoreEntry e : toAdd) {
|
||||||
e.initializeEntry();
|
e.initializeEntry();
|
||||||
}
|
}
|
||||||
refreshValidities(true);
|
|
||||||
saveAsync();
|
saveAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -598,7 +604,7 @@ public abstract class DataStorage {
|
||||||
this.storeEntries.remove(store);
|
this.storeEntries.remove(store);
|
||||||
getDefaultDisplayParent(store).ifPresent(p -> p.setChildrenCache(null));
|
getDefaultDisplayParent(store).ifPresent(p -> p.setChildrenCache(null));
|
||||||
this.listeners.forEach(l -> l.onStoreRemove(store));
|
this.listeners.forEach(l -> l.onStoreRemove(store));
|
||||||
refreshValidities(false);
|
refreshEntries();
|
||||||
saveAsync();
|
saveAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -734,6 +740,22 @@ public abstract class DataStorage {
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<DataStoreCategory> getCategoryParentHierarchy(DataStoreCategory cat) {
|
||||||
|
var es = new ArrayList<DataStoreCategory>();
|
||||||
|
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<DataStoreEntry> getStoreParentHierarchy(DataStoreEntry entry) {
|
public List<DataStoreEntry> getStoreParentHierarchy(DataStoreEntry entry) {
|
||||||
var es = new ArrayList<DataStoreEntry>();
|
var es = new ArrayList<DataStoreEntry>();
|
||||||
es.add(entry);
|
es.add(entry);
|
||||||
|
@ -750,34 +772,17 @@ public abstract class DataStorage {
|
||||||
return es;
|
return es;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DataStoreId getId(DataStoreEntry entry) {
|
public StorePath getStorePath(DataStoreEntry entry) {
|
||||||
return DataStoreId.create(getStoreParentHierarchy(entry).stream()
|
return StorePath.create(getStoreParentHierarchy(entry).stream()
|
||||||
.filter(e -> !(e.getStore() instanceof LocalStore))
|
.filter(e -> !(e.getStore() instanceof LocalStore))
|
||||||
.map(e -> e.getName().replaceAll(":", "_"))
|
.map(e -> e.getName().toLowerCase().replaceAll("/", "_"))
|
||||||
.toArray(String[]::new));
|
.toArray(String[]::new));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<DataStoreEntry> getStoreEntryIfPresent(@NonNull DataStoreId id) {
|
public StorePath getStorePath(DataStoreCategory entry) {
|
||||||
var current = getStoreEntryIfPresent(id.getNames().getFirst());
|
return StorePath.create(getCategoryParentHierarchy(entry).stream()
|
||||||
if (current.isPresent()) {
|
.map(e -> e.getName().toLowerCase().replaceAll("/", "_"))
|
||||||
for (int i = 1; i < id.getNames().size(); i++) {
|
.toArray(String[]::new));
|
||||||
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 Optional<DataStoreEntry> getStoreEntryInProgressIfPresent(@NonNull DataStore store) {
|
public Optional<DataStoreEntry> getStoreEntryInProgressIfPresent(@NonNull DataStore store) {
|
||||||
|
@ -787,12 +792,29 @@ public abstract class DataStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<DataStoreEntry> getStoreEntryIfPresent(@NonNull DataStore store, boolean identityOnly) {
|
public Optional<DataStoreEntry> 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
|
.filter(n -> n.getStore() == store || (!identityOnly && (n.getStore() != null
|
||||||
&& Objects.equals(
|
&& Objects.equals(
|
||||||
store.getClass(), n.getStore().getClass())
|
store.getClass(), n.getStore().getClass())
|
||||||
&& store.equals(n.getStore()))))
|
&& store.equals(n.getStore()))))
|
||||||
.findFirst();
|
.findFirst();
|
||||||
|
if (found.isPresent()) {
|
||||||
|
if (identityOnly) {
|
||||||
|
synchronized (storeEntryMapCache) {
|
||||||
|
storeEntryMapCache.put(store, found.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DataStoreCategory getRootCategory(DataStoreCategory category) {
|
public DataStoreCategory getRootCategory(DataStoreCategory category) {
|
||||||
|
@ -854,12 +876,22 @@ public abstract class DataStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
public DataStoreEntry getOrCreateNewSyntheticEntry(DataStoreEntry parent, String name, DataStore store) {
|
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 uuid = UuidHelper.generateFromObject(parent.getUuid(), name);
|
||||||
var found = getStoreEntryIfPresent(uuid);
|
var found = getStoreEntryIfPresent(uuid);
|
||||||
if (found.isPresent()) {
|
if (found.isPresent()) {
|
||||||
return found.get();
|
return found.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var forStore = getStoreEntryIfPresent(store, false);
|
||||||
|
if (forStore.isPresent()) {
|
||||||
|
return forStore.get();
|
||||||
|
}
|
||||||
|
|
||||||
return DataStoreEntry.createNew(uuid, parent.getCategoryUuid(), name, store);
|
return DataStoreEntry.createNew(uuid, parent.getCategoryUuid(), name, store);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
@ -46,8 +47,7 @@ public class DataStoreEntry extends StorageElement {
|
||||||
@NonFinal
|
@NonFinal
|
||||||
boolean expanded;
|
boolean expanded;
|
||||||
|
|
||||||
@NonFinal
|
AtomicInteger busyCounter = new AtomicInteger();
|
||||||
boolean inRefresh;
|
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@NonFinal
|
@NonFinal
|
||||||
|
@ -323,14 +323,21 @@ public class DataStoreEntry extends StorageElement {
|
||||||
return getName();
|
return getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setInRefresh(boolean newRefresh) {
|
public void incrementBusyCounter() {
|
||||||
var changed = inRefresh != newRefresh;
|
var r = busyCounter.incrementAndGet() == 1;
|
||||||
if (changed) {
|
if (r) {
|
||||||
this.inRefresh = newRefresh;
|
|
||||||
notifyUpdate(false, false);
|
notifyUpdate(false, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean decrementBusyCounter() {
|
||||||
|
var r = busyCounter.decrementAndGet() == 0;
|
||||||
|
if (r) {
|
||||||
|
notifyUpdate(false, false);
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
public <T extends DataStore> DataStoreEntryRef<T> ref() {
|
public <T extends DataStore> DataStoreEntryRef<T> ref() {
|
||||||
return new DataStoreEntryRef<>(this);
|
return new DataStoreEntryRef<>(this);
|
||||||
}
|
}
|
||||||
|
@ -503,7 +510,7 @@ public class DataStoreEntry extends StorageElement {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
store.checkComplete();
|
store.checkComplete();
|
||||||
setInRefresh(true);
|
incrementBusyCounter();
|
||||||
if (store instanceof ValidatableStore l) {
|
if (store instanceof ValidatableStore l) {
|
||||||
l.validate();
|
l.validate();
|
||||||
} else if (store instanceof FixedHierarchyStore h) {
|
} else if (store instanceof FixedHierarchyStore h) {
|
||||||
|
@ -512,32 +519,27 @@ public class DataStoreEntry extends StorageElement {
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setInRefresh(false);
|
decrementBusyCounter();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean tryMakeValid() {
|
public void refreshStore() {
|
||||||
if (validity == Validity.LOAD_FAILED) {
|
if (validity == Validity.LOAD_FAILED) {
|
||||||
return false;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
var complete = validity == Validity.COMPLETE;
|
|
||||||
if (complete) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var newStore = DataStorageParser.storeFromNode(storeNode);
|
var newStore = DataStorageParser.storeFromNode(storeNode);
|
||||||
if (newStore == null) {
|
if (newStore == null) {
|
||||||
store = null;
|
store = null;
|
||||||
validity = Validity.LOAD_FAILED;
|
validity = Validity.LOAD_FAILED;
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var newComplete = newStore.isComplete();
|
var newComplete = newStore.isComplete();
|
||||||
if (!newComplete) {
|
if (!newComplete) {
|
||||||
validity = Validity.INCOMPLETE;
|
validity = Validity.INCOMPLETE;
|
||||||
store = newStore;
|
store = newStore;
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newStore.equals(store)) {
|
if (!newStore.equals(store)) {
|
||||||
|
@ -546,52 +548,19 @@ public class DataStoreEntry extends StorageElement {
|
||||||
validity = Validity.COMPLETE;
|
validity = Validity.COMPLETE;
|
||||||
// Don't count this as modification as this is done always
|
// Don't count this as modification as this is done always
|
||||||
notifyUpdate(false, false);
|
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
|
@SneakyThrows
|
||||||
public void initializeEntry() {
|
public void initializeEntry() {
|
||||||
if (store instanceof ExpandedLifecycleStore lifecycleStore) {
|
if (store instanceof ExpandedLifecycleStore lifecycleStore) {
|
||||||
try {
|
try {
|
||||||
inRefresh = true;
|
incrementBusyCounter();
|
||||||
notifyUpdate(false, false);
|
notifyUpdate(false, false);
|
||||||
lifecycleStore.initializeValidate();
|
lifecycleStore.initializeValidate();
|
||||||
inRefresh = false;
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
inRefresh = false;
|
|
||||||
ErrorEvent.fromThrowable(e).handle();
|
ErrorEvent.fromThrowable(e).handle();
|
||||||
} finally {
|
} finally {
|
||||||
|
decrementBusyCounter();
|
||||||
notifyUpdate(false, false);
|
notifyUpdate(false, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -601,12 +570,13 @@ public class DataStoreEntry extends StorageElement {
|
||||||
public void finalizeEntry() {
|
public void finalizeEntry() {
|
||||||
if (store instanceof ExpandedLifecycleStore lifecycleStore) {
|
if (store instanceof ExpandedLifecycleStore lifecycleStore) {
|
||||||
try {
|
try {
|
||||||
inRefresh = true;
|
incrementBusyCounter();
|
||||||
notifyUpdate(false, false);
|
notifyUpdate(false, false);
|
||||||
lifecycleStore.finalizeValidate();
|
lifecycleStore.finalizeValidate();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
ErrorEvent.fromThrowable(e).handle();
|
ErrorEvent.fromThrowable(e).handle();
|
||||||
} finally {
|
} finally {
|
||||||
|
decrementBusyCounter();
|
||||||
notifyUpdate(false, false);
|
notifyUpdate(false, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
package io.xpipe.app.storage;
|
package io.xpipe.app.storage;
|
||||||
|
|
||||||
import io.xpipe.app.comp.store.StoreSortMode;
|
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 io.xpipe.core.store.LocalStore;
|
||||||
|
|
||||||
import org.apache.commons.io.FileUtils;
|
|
||||||
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ -20,22 +15,17 @@ public class ImpersistentStorage extends DataStorage {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void load() {
|
public void load() {
|
||||||
var storesDir = getStoresDir();
|
|
||||||
var categoriesDir = getCategoriesDir();
|
|
||||||
|
|
||||||
{
|
{
|
||||||
var cat = DataStoreCategory.createNew(null, ALL_CONNECTIONS_CATEGORY_UUID, "All connections");
|
var cat = DataStoreCategory.createNew(null, ALL_CONNECTIONS_CATEGORY_UUID, "All connections");
|
||||||
cat.setDirectory(categoriesDir.resolve(ALL_CONNECTIONS_CATEGORY_UUID.toString()));
|
|
||||||
storeCategories.add(cat);
|
storeCategories.add(cat);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
var cat = DataStoreCategory.createNew(null, ALL_SCRIPTS_CATEGORY_UUID, "All scripts");
|
var cat = DataStoreCategory.createNew(null, ALL_SCRIPTS_CATEGORY_UUID, "All scripts");
|
||||||
cat.setDirectory(categoriesDir.resolve(ALL_SCRIPTS_CATEGORY_UUID.toString()));
|
|
||||||
storeCategories.add(cat);
|
storeCategories.add(cat);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
var cat = new DataStoreCategory(
|
var cat = new DataStoreCategory(
|
||||||
categoriesDir.resolve(DEFAULT_CATEGORY_UUID.toString()),
|
null,
|
||||||
DEFAULT_CATEGORY_UUID,
|
DEFAULT_CATEGORY_UUID,
|
||||||
"Default",
|
"Default",
|
||||||
Instant.now(),
|
Instant.now(),
|
||||||
|
@ -50,7 +40,6 @@ public class ImpersistentStorage extends DataStorage {
|
||||||
|
|
||||||
var e = DataStoreEntry.createNew(
|
var e = DataStoreEntry.createNew(
|
||||||
LOCAL_ID, DataStorage.DEFAULT_CATEGORY_UUID, "Local Machine", new LocalStore());
|
LOCAL_ID, DataStorage.DEFAULT_CATEGORY_UUID, "Local Machine", new LocalStore());
|
||||||
e.setDirectory(getStoresDir().resolve(LOCAL_ID.toString()));
|
|
||||||
e.setConfiguration(
|
e.setConfiguration(
|
||||||
StorageElement.Configuration.builder().deletable(false).build());
|
StorageElement.Configuration.builder().deletable(false).build());
|
||||||
storeEntries.put(e, e);
|
storeEntries.put(e, e);
|
||||||
|
@ -58,18 +47,7 @@ public class ImpersistentStorage extends DataStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized void save(boolean dispose) {
|
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supportsSharing() {
|
public boolean supportsSharing() {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package io.xpipe.app.storage;
|
package io.xpipe.app.storage;
|
||||||
|
|
||||||
|
import io.xpipe.app.ext.DataStorageExtensionProvider;
|
||||||
import io.xpipe.app.issue.ErrorEvent;
|
import io.xpipe.app.issue.ErrorEvent;
|
||||||
import io.xpipe.app.issue.TrackEvent;
|
import io.xpipe.app.issue.TrackEvent;
|
||||||
import io.xpipe.app.prefs.AppPrefs;
|
import io.xpipe.app.prefs.AppPrefs;
|
||||||
import io.xpipe.core.process.OsType;
|
import io.xpipe.core.process.OsType;
|
||||||
import io.xpipe.core.store.LocalStore;
|
import io.xpipe.core.store.LocalStore;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
|
|
||||||
|
@ -197,14 +197,15 @@ public class StandardStorage extends DataStorage {
|
||||||
local.setColor(DataStoreColor.BLUE);
|
local.setColor(DataStoreColor.BLUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshValidities(true);
|
callProviders();
|
||||||
|
refreshEntries();
|
||||||
storeEntriesSet.forEach(entry -> {
|
storeEntriesSet.forEach(entry -> {
|
||||||
var syntheticParent = getSyntheticParent(entry);
|
var syntheticParent = getSyntheticParent(entry);
|
||||||
syntheticParent.ifPresent(entry1 -> {
|
syntheticParent.ifPresent(entry1 -> {
|
||||||
addStoreEntryIfNotPresent(entry1);
|
addStoreEntryIfNotPresent(entry1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
refreshValidities(true);
|
refreshEntries();
|
||||||
|
|
||||||
// Save to apply changes
|
// Save to apply changes
|
||||||
if (!hasFixedLocal) {
|
if (!hasFixedLocal) {
|
||||||
|
@ -226,6 +227,16 @@ public class StandardStorage extends DataStorage {
|
||||||
this.gitStorageHandler.afterStorageLoad();
|
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) {
|
public void save(boolean dispose) {
|
||||||
try {
|
try {
|
||||||
// If another save operation is in progress, we have to wait on dispose
|
// If another save operation is in progress, we have to wait on dispose
|
||||||
|
|
|
@ -238,6 +238,35 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
||||||
.addFile(configuration.getScriptFile());
|
.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) {
|
ExternalTerminalType ELEMENTARY = new SimplePathType("app.elementaryTerminal", "io.elementary.terminal", true) {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -262,7 +291,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
|
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) {
|
ExternalTerminalType TILIX = new SimplePathType("app.tilix", "tilix", true) {
|
||||||
|
@ -514,17 +543,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||||
try (ShellControl pc = LocalShell.getShell()) {
|
LocalShell.getShell()
|
||||||
var suffix = "\"" + configuration.getScriptFile().toString().replaceAll("\"", "\\\\\"") + "\"";
|
.executeSimpleCommand(CommandBuilder.of()
|
||||||
pc.osascriptCommand(String.format(
|
.add("open", "-a")
|
||||||
"""
|
.addQuoted("Terminal.app")
|
||||||
activate application "Terminal"
|
.addFile(configuration.getScriptFile()));
|
||||||
delay 1
|
|
||||||
tell app "Terminal" to do script %s
|
|
||||||
""",
|
|
||||||
suffix))
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ExternalTerminalType ITERM2 = new MacOsType("app.iterm2", "iTerm") {
|
ExternalTerminalType ITERM2 = new MacOsType("app.iterm2", "iTerm") {
|
||||||
|
@ -550,26 +573,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||||
try (ShellControl pc = LocalShell.getShell()) {
|
LocalShell.getShell()
|
||||||
pc.osascriptCommand(String.format(
|
.executeSimpleCommand(CommandBuilder.of()
|
||||||
"""
|
.add("open", "-a")
|
||||||
if application "iTerm" is not running then
|
.addQuoted("iTerm.app")
|
||||||
launch application "iTerm"
|
.addFile(configuration.getScriptFile()));
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ExternalTerminalType WARP = new MacOsType("app.warp", "Warp") {
|
ExternalTerminalType WARP = new MacOsType("app.warp", "Warp") {
|
||||||
|
@ -662,6 +670,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
||||||
TILDA,
|
TILDA,
|
||||||
XTERM,
|
XTERM,
|
||||||
DEEPIN_TERMINAL,
|
DEEPIN_TERMINAL,
|
||||||
|
FOOT,
|
||||||
Q_TERMINAL);
|
Q_TERMINAL);
|
||||||
List<ExternalTerminalType> MACOS_TERMINALS = List.of(
|
List<ExternalTerminalType> MACOS_TERMINALS = List.of(
|
||||||
KittyTerminalType.KITTY_MACOS,
|
KittyTerminalType.KITTY_MACOS,
|
||||||
|
@ -705,7 +714,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
||||||
return ExternalTerminalType.POWERSHELL;
|
return ExternalTerminalType.POWERSHELL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existing != null) {
|
// Verify that our selection is still valid
|
||||||
|
if (existing != null && existing.isAvailable()) {
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
package io.xpipe.app.terminal;
|
package io.xpipe.app.terminal;
|
||||||
|
|
||||||
|
import io.xpipe.app.issue.ErrorEvent;
|
||||||
import io.xpipe.app.prefs.ExternalApplicationHelper;
|
import io.xpipe.app.prefs.ExternalApplicationHelper;
|
||||||
|
import io.xpipe.app.prefs.ExternalApplicationType;
|
||||||
import io.xpipe.app.util.LocalShell;
|
import io.xpipe.app.util.LocalShell;
|
||||||
|
import io.xpipe.app.util.ThreadHelper;
|
||||||
import io.xpipe.app.util.WindowsRegistry;
|
import io.xpipe.app.util.WindowsRegistry;
|
||||||
import io.xpipe.core.process.CommandBuilder;
|
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.nio.file.Path;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
@ -26,7 +31,7 @@ public interface WezTerminalType extends ExternalTerminalType {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
default boolean isRecommended() {
|
default boolean isRecommended() {
|
||||||
return false;
|
return OsType.getLocal() != OsType.WINDOWS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -51,25 +56,62 @@ public interface WezTerminalType extends ExternalTerminalType {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Optional<Path> determineInstallation() {
|
protected Optional<Path> determineInstallation() {
|
||||||
Optional<String> launcherDir;
|
try {
|
||||||
launcherDir = WindowsRegistry.local().readValue(
|
var foundKey = WindowsRegistry.local().findKeyForEqualValueMatchRecursive(WindowsRegistry.HKEY_LOCAL_MACHINE,
|
||||||
WindowsRegistry.HKEY_LOCAL_MACHINE,
|
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "http://wezfurlong.org/wezterm");
|
||||||
"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{BCF6F0DA-5B9A-408D-8562-F680AE6E1EAF}_is1",
|
if (foundKey.isPresent()) {
|
||||||
"InstallLocation")
|
var installKey = WindowsRegistry.local().readValue(
|
||||||
.map(p -> p + "\\wezterm-gui.exe");
|
foundKey.get().getHkey(),
|
||||||
return launcherDir.map(Path::of);
|
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() {
|
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
|
@Override
|
||||||
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
|
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||||
return CommandBuilder.of().add("start").addFile(configuration.getScriptFile());
|
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
|
@Override
|
||||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||||
var path = LocalShell.getShell()
|
try (var sc = LocalShell.getShell()) {
|
||||||
.command(String.format(
|
var path = sc.command(
|
||||||
"mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications 2>/dev/null",
|
String.format("mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications 2>/dev/null",
|
||||||
applicationName))
|
applicationName)).readStdoutOrThrow();
|
||||||
.readStdoutOrThrow();
|
var spawn = sc.command(CommandBuilder.of().addFile(Path.of(path)
|
||||||
var c = CommandBuilder.of()
|
.resolve("Contents")
|
||||||
.addFile(Path.of(path)
|
.resolve("MacOS")
|
||||||
.resolve("Contents")
|
.resolve("wezterm").toString())
|
||||||
.resolve("MacOS")
|
.add("cli", "spawn", "--pane-id", "0")
|
||||||
.resolve("wezterm-gui")
|
.addFile(configuration.getScriptFile()))
|
||||||
.toString())
|
.executeAndCheck();
|
||||||
.add("start")
|
if (!spawn) {
|
||||||
.add(configuration.getDialectLaunchCommand());
|
ExternalApplicationHelper.startAsync(CommandBuilder.of()
|
||||||
ExternalApplicationHelper.startAsync(c);
|
.addFile(Path.of(path)
|
||||||
|
.resolve("Contents")
|
||||||
|
.resolve("MacOS")
|
||||||
|
.resolve("wezterm-gui").toString())
|
||||||
|
.add("start")
|
||||||
|
.addFile(configuration.getScriptFile()));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,10 @@ public class AskpassAlert {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle(long now) {
|
public void handle(long now) {
|
||||||
|
if (!stage.isShowing()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (regainedFocusCount >= 2) {
|
if (regainedFocusCount >= 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,21 @@
|
||||||
package io.xpipe.app.util;
|
package io.xpipe.app.util;
|
||||||
|
|
||||||
import io.xpipe.core.process.CommandBuilder;
|
import io.xpipe.core.process.CommandBuilder;
|
||||||
|
import io.xpipe.core.process.CommandControl;
|
||||||
import io.xpipe.core.process.ShellControl;
|
import io.xpipe.core.process.ShellControl;
|
||||||
import lombok.experimental.SuperBuilder;
|
|
||||||
|
|
||||||
@SuperBuilder
|
import java.util.function.Consumer;
|
||||||
public abstract class CommandView {
|
|
||||||
|
|
||||||
protected final ShellControl shellControl;
|
public abstract class CommandView implements AutoCloseable {
|
||||||
|
|
||||||
protected abstract CommandBuilder base();
|
protected abstract CommandControl build(Consumer<CommandBuilder> builder);
|
||||||
|
|
||||||
|
protected abstract ShellControl getShellControl();
|
||||||
|
|
||||||
|
public abstract CommandView start() throws Exception;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws Exception {
|
||||||
|
getShellControl().close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
18
app/src/main/java/io/xpipe/app/util/CommandViewBase.java
Normal file
18
app/src/main/java/io/xpipe/app/util/CommandViewBase.java
Normal file
|
@ -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<CommandBuilder> builder);
|
||||||
|
}
|
|
@ -28,10 +28,9 @@ public class FileOpener {
|
||||||
try {
|
try {
|
||||||
editor.launch(Path.of(localFile).toRealPath());
|
editor.launch(Path.of(localFile).toRealPath());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
ErrorEvent.fromThrowable(e)
|
ErrorEvent.fromThrowable("Unable to launch editor "
|
||||||
.description("Unable to launch editor "
|
|
||||||
+ editor.toTranslatedString().getValue()
|
+ 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()
|
.expected()
|
||||||
.handle();
|
.handle();
|
||||||
}
|
}
|
||||||
|
@ -52,8 +51,7 @@ public class FileOpener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
ErrorEvent.fromThrowable(e)
|
ErrorEvent.fromThrowable("Unable to open file " + localFile, e)
|
||||||
.description("Unable to open file " + localFile)
|
|
||||||
.handle();
|
.handle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,8 +66,7 @@ public class FileOpener {
|
||||||
pc.executeSimpleCommand("open \"" + localFile + "\"");
|
pc.executeSimpleCommand("open \"" + localFile + "\"");
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
ErrorEvent.fromThrowable(e)
|
ErrorEvent.fromThrowable("Unable to open file " + localFile, e)
|
||||||
.description("Unable to open file " + localFile)
|
|
||||||
.handle();
|
.handle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,14 @@ import java.util.Locale;
|
||||||
|
|
||||||
public class HostHelper {
|
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 {
|
public static int findRandomOpenPortOnAllLocalInterfaces() throws IOException {
|
||||||
try (ServerSocket socket = new ServerSocket(0)) {
|
try (ServerSocket socket = new ServerSocket(0)) {
|
||||||
return socket.getLocalPort();
|
return socket.getLocalPort();
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
package io.xpipe.app.util;
|
package io.xpipe.app.util;
|
||||||
|
|
||||||
import io.xpipe.app.core.AppI18n;
|
import io.xpipe.app.core.AppI18n;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
|
@ -15,6 +14,12 @@ public class LicenseRequiredException extends RuntimeException {
|
||||||
this.feature = feature;
|
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) {
|
public LicenseRequiredException(String featureName, boolean plural, LicensedFeature feature) {
|
||||||
super(featureName + " " + (plural ? AppI18n.get("areOnlySupported") : AppI18n.get("isOnlySupported")));
|
super(featureName + " " + (plural ? AppI18n.get("areOnlySupported") : AppI18n.get("isOnlySupported")));
|
||||||
this.feature = feature;
|
this.feature = feature;
|
||||||
|
|
|
@ -41,6 +41,7 @@ public class LocalShell {
|
||||||
localPowershell = ProcessControlProvider.get()
|
localPowershell = ProcessControlProvider.get()
|
||||||
.createLocalProcessControl(false)
|
.createLocalProcessControl(false)
|
||||||
.subShell(ShellDialects.POWERSHELL)
|
.subShell(ShellDialects.POWERSHELL)
|
||||||
|
.withoutLicenseCheck()
|
||||||
.start();
|
.start();
|
||||||
}
|
}
|
||||||
return localPowershell.start();
|
return localPowershell.start();
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue