mirror of
https://github.com/xpipe-io/xpipe.git
synced 2026-05-29 07:20:35 +00:00
Squash merge branch 18-release into master
This commit is contained in:
+44
-38
@@ -23,8 +23,8 @@ dependencies {
|
||||
api project(':beacon')
|
||||
|
||||
compileOnly 'org.hamcrest:hamcrest:3.0'
|
||||
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.12.2'
|
||||
compileOnly 'org.junit.jupiter:junit-jupiter-params:5.12.2'
|
||||
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.13.4'
|
||||
compileOnly 'org.junit.jupiter:junit-jupiter-params:5.13.4'
|
||||
|
||||
api 'com.vladsch.flexmark:flexmark:0.64.8'
|
||||
api 'com.vladsch.flexmark:flexmark-util:0.64.8'
|
||||
@@ -48,20 +48,30 @@ dependencies {
|
||||
api 'com.vladsch.flexmark:flexmark-ext-yaml-front-matter:0.64.8'
|
||||
api 'com.vladsch.flexmark:flexmark-ext-toc:0.64.8'
|
||||
|
||||
api("com.github.weisj:jsvg:1.7.1")
|
||||
api ('io.modelcontextprotocol.sdk:mcp:0.11.0') {
|
||||
exclude group: "com.ethlo.time", module: "itu"
|
||||
exclude group: "com.fasterxml.jackson.dataformat", module: "jackson-dataformat-yaml"
|
||||
}
|
||||
api "io.projectreactor:reactor-core:3.7.0"
|
||||
api "org.reactivestreams:reactive-streams:1.0.4"
|
||||
api ("com.networknt:json-schema-validator:1.5.8") {
|
||||
exclude group: "com.ethlo.time", module: "itu"
|
||||
exclude group: "com.fasterxml.jackson.dataformat", module: "jackson-dataformat-yaml"
|
||||
}
|
||||
|
||||
api "com.github.weisj:jsvg:1.7.1"
|
||||
api 'io.xpipe:vernacular:1.15'
|
||||
api 'org.bouncycastle:bcprov-jdk18on:1.81'
|
||||
api 'info.picocli:picocli:4.7.6'
|
||||
api 'org.apache.commons:commons-lang3:3.17.0'
|
||||
api 'org.apache.commons:commons-lang3:3.18.0'
|
||||
api 'io.sentry:sentry:8.13.3'
|
||||
api 'commons-io:commons-io:2.19.0'
|
||||
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.19.1"
|
||||
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.19.1"
|
||||
api group: 'org.kordamp.ikonli', name: 'ikonli-material2-pack', version: "12.2.0"
|
||||
api group: 'org.kordamp.ikonli', name: 'ikonli-materialdesign2-pack', version: "12.2.0"
|
||||
api group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0"
|
||||
api group: 'org.kordamp.ikonli', name: 'ikonli-material-pack', version: "12.2.0"
|
||||
api group: 'org.kordamp.ikonli', name: 'ikonli-feather-pack', version: "12.2.0"
|
||||
api group: 'org.kordamp.ikonli', name: 'ikonli-material2-pack', version: "12.4.0"
|
||||
api group: 'org.kordamp.ikonli', name: 'ikonli-materialdesign2-pack', version: "12.4.0"
|
||||
api 'org.kordamp.ikonli:ikonli-bootstrapicons-pack:12.4.0'
|
||||
api group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.4.0"
|
||||
api group: 'org.slf4j', name: 'slf4j-api', version: '2.0.17'
|
||||
api group: 'org.slf4j', name: 'slf4j-jdk-platform-logging', version: '2.0.17'
|
||||
api 'io.xpipe:modulefs:0.1.6'
|
||||
@@ -71,31 +81,30 @@ dependencies {
|
||||
|
||||
apply from: "$rootDir/gradle/gradle_scripts/local_junit_suite.gradle"
|
||||
|
||||
def extensionJarDepList = project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)).toList();
|
||||
def extensionJarDepList = project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)).toList()
|
||||
jar {
|
||||
finalizedBy(extensionJarDepList)
|
||||
}
|
||||
|
||||
application {
|
||||
mainModule = 'io.xpipe.app'
|
||||
mainClass = 'io.xpipe.app.Main'
|
||||
mainModule = groupName + '.app'
|
||||
mainClass = groupName + '.app.Main'
|
||||
applicationDefaultJvmArgs = jvmRunArgs
|
||||
}
|
||||
|
||||
run {
|
||||
systemProperty 'io.xpipe.app.useVirtualThreads', 'false'
|
||||
systemProperty 'io.xpipe.app.mode', 'gui'
|
||||
systemProperty 'io.xpipe.app.writeLogs', "true"
|
||||
systemProperty 'io.xpipe.app.writeSysOut', "true"
|
||||
systemProperty 'io.xpipe.app.developerMode', "true"
|
||||
systemProperty 'io.xpipe.app.logLevel', "trace"
|
||||
systemProperty 'io.xpipe.app.fullVersion', rootProject.fullVersion
|
||||
systemProperty 'io.xpipe.app.staging', isStage
|
||||
// systemProperty 'io.xpipe.beacon.port', "30000"
|
||||
systemProperty propertyName('useVirtualThreads'), 'false'
|
||||
systemProperty propertyName('mode'), 'gui'
|
||||
systemProperty propertyName('writeLogs'), "true"
|
||||
systemProperty propertyName('writeSysOut'), "true"
|
||||
systemProperty propertyName('developerMode'), "true"
|
||||
systemProperty propertyName('logLevel'), "trace"
|
||||
systemProperty propertyName('fullVersion'), fullVersion
|
||||
systemProperty propertyName('staging'), isStage
|
||||
|
||||
// Apply passed xpipe properties
|
||||
for (final def e in System.getProperties().entrySet()) {
|
||||
if (e.getKey().toString().contains("xpipe")) {
|
||||
if (e.getKey().toString().contains(snakeProductName)) {
|
||||
systemProperty e.getKey().toString(), e.getValue()
|
||||
}
|
||||
}
|
||||
@@ -103,17 +112,17 @@ run {
|
||||
workingDir = rootDir
|
||||
jvmArgs += ['-XX:+EnableDynamicAgentLoading']
|
||||
|
||||
def exts = files(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0].outputs.files.singleFile).toList());
|
||||
def exts = files(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0].outputs.files.singleFile).toList())
|
||||
classpath += exts
|
||||
|
||||
dependsOn(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0]).toList())
|
||||
}
|
||||
|
||||
task runAttachedDebugger(type: JavaExec) {
|
||||
tasks.register('runAttachedDebugger', JavaExec) {
|
||||
workingDir = rootDir
|
||||
classpath = run.classpath
|
||||
mainModule = 'io.xpipe.app'
|
||||
mainClass = 'io.xpipe.app.Main'
|
||||
mainModule = groupName + '.app'
|
||||
mainClass = groupName + '.app.Main'
|
||||
modularity.inferModulePath = true
|
||||
jvmArgs += jvmRunArgs
|
||||
jvmArgs += List.of(
|
||||
@@ -123,7 +132,7 @@ task runAttachedDebugger(type: JavaExec) {
|
||||
jvmArgs += ['-XX:+EnableDynamicAgentLoading']
|
||||
systemProperties run.systemProperties
|
||||
|
||||
def exts = files(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0].outputs.files.singleFile).toList());
|
||||
def exts = files(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0].outputs.files.singleFile).toList())
|
||||
classpath += exts
|
||||
dependsOn(project.allExtensions.stream().map(p -> p.getTasksByName('jar', true)[0]).toList())
|
||||
|
||||
@@ -134,15 +143,12 @@ processResources {
|
||||
def cssFiles = fileTree(dir: "$sourceSets.main.output.resourcesDir/io/xpipe/app/resources/style")
|
||||
cssFiles.include "**/*.css"
|
||||
cssFiles.each { css ->
|
||||
logger.info("converting CSS to BSS ${css}");
|
||||
|
||||
javaexec {
|
||||
workingDir = project.projectDir
|
||||
jvmArgs += "--module-path=${configurations.javafx.asFileTree.asPath},"
|
||||
jvmArgs += "--add-modules=javafx.graphics"
|
||||
main = "com.sun.javafx.css.parser.Css2Bin"
|
||||
providers.javaexec {
|
||||
workingDir = projectDir
|
||||
jvmArgs += ["--module-path=${configurations.javafx.asFileTree.asPath},", "--add-modules=javafx.graphics"]
|
||||
mainClass = "com.sun.javafx.css.parser.Css2Bin"
|
||||
args css
|
||||
}
|
||||
}.result.get()
|
||||
|
||||
delete css
|
||||
}
|
||||
@@ -159,13 +165,13 @@ processResources {
|
||||
}
|
||||
|
||||
distTar {
|
||||
enabled = false;
|
||||
enabled = false
|
||||
}
|
||||
|
||||
distZip {
|
||||
enabled = false;
|
||||
enabled = false
|
||||
}
|
||||
|
||||
assembleDist {
|
||||
enabled = false;
|
||||
enabled = false
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.util.DataStoreFormatter;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
import io.xpipe.app.util.LicensedFeature;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import lombok.experimental.SuperBuilder;
|
||||
@@ -24,11 +25,6 @@ public abstract class AbstractAction {
|
||||
private static boolean closed;
|
||||
private static Consumer<AbstractAction> pick;
|
||||
|
||||
private static final AppLayoutModel.QueueEntry queueEntry = new AppLayoutModel.QueueEntry(
|
||||
AppI18n.observable("cancelActionPicker"), new LabelGraphic.IconGraphic("mdal-cancel_presentation"), () -> {
|
||||
cancelPick();
|
||||
});
|
||||
|
||||
public static synchronized void expectPick() {
|
||||
if (pick != null) {
|
||||
return;
|
||||
@@ -50,6 +46,11 @@ public abstract class AbstractAction {
|
||||
};
|
||||
}
|
||||
|
||||
private static final AppLayoutModel.QueueEntry queueEntry = new AppLayoutModel.QueueEntry(
|
||||
AppI18n.observable("cancelActionPicker"), new LabelGraphic.IconGraphic("mdal-cancel_presentation"), () -> {
|
||||
cancelPick();
|
||||
});
|
||||
|
||||
public static synchronized void cancelPick() {
|
||||
AppLayoutModel.get().getQueueEntries().remove(queueEntry);
|
||||
pick = null;
|
||||
@@ -121,6 +122,8 @@ public abstract class AbstractAction {
|
||||
return false;
|
||||
}
|
||||
|
||||
checkLicense();
|
||||
|
||||
synchronized (active) {
|
||||
active.add(this);
|
||||
}
|
||||
@@ -147,8 +150,6 @@ public abstract class AbstractAction {
|
||||
synchronized (active) {
|
||||
active.remove(this);
|
||||
}
|
||||
|
||||
TrackEvent.withTrace("Finished action execution").tag("id", getId()).handle();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +192,17 @@ public abstract class AbstractAction {
|
||||
return false;
|
||||
}
|
||||
|
||||
public LicensedFeature getLicensedFeature() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void checkLicense() {
|
||||
var feature = getLicensedFeature();
|
||||
if (feature != null) {
|
||||
feature.throwIfUnsupported();
|
||||
}
|
||||
}
|
||||
|
||||
protected void afterExecute() {}
|
||||
|
||||
public abstract Map<String, String> toDisplayMap();
|
||||
|
||||
@@ -130,16 +130,20 @@ public class ActionJacksonMapper {
|
||||
}
|
||||
|
||||
var tree = (ObjectNode) JacksonMapper.getDefault().valueToTree(value);
|
||||
var treeCopy = JsonNodeFactory.instance.objectNode();
|
||||
treeCopy.put("id", value.getId());
|
||||
tree.properties().forEach(p -> {
|
||||
treeCopy.set(p.getKey(), p.getValue());
|
||||
});
|
||||
|
||||
if (value instanceof MultiStoreAction<?> m) {
|
||||
var refs = tree.get("refs");
|
||||
tree.remove("refs");
|
||||
tree.set("ref", refs);
|
||||
tree.put("id", m.getId());
|
||||
return tree;
|
||||
var refs = treeCopy.get("refs");
|
||||
treeCopy.remove("refs");
|
||||
treeCopy.set("ref", refs);
|
||||
treeCopy.put("id", m.getId());
|
||||
return treeCopy;
|
||||
}
|
||||
|
||||
tree.put("id", value.getId());
|
||||
return tree;
|
||||
return treeCopy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,10 +37,6 @@ public interface ActionProvider {
|
||||
|
||||
default void init() {}
|
||||
|
||||
default String getLicensedFeatureId() {
|
||||
return null;
|
||||
}
|
||||
|
||||
default String getId() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package io.xpipe.app.beacon;
|
||||
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.beacon.BeaconClientException;
|
||||
|
||||
import lombok.Value;
|
||||
@@ -23,4 +25,27 @@ public class AppBeaconCache {
|
||||
}
|
||||
return found.get();
|
||||
}
|
||||
|
||||
public BeaconShellSession getOrStart(DataStoreEntryRef<ShellStore> ref) throws Exception {
|
||||
var existing = AppBeaconServer.get().getCache().getShellSessions().stream()
|
||||
.filter(beaconShellSession -> beaconShellSession.getEntry().equals(ref.get()))
|
||||
.findFirst();
|
||||
var control = (existing.isPresent()
|
||||
? existing.get().getControl()
|
||||
: ref.getStore().standaloneControl().start());
|
||||
control.setNonInteractive();
|
||||
control.start();
|
||||
|
||||
var d = control.getShellDialect().getDumbMode();
|
||||
if (!d.supportsAnyPossibleInteraction()) {
|
||||
control.close();
|
||||
d.throwIfUnsupported();
|
||||
}
|
||||
|
||||
if (existing.isEmpty()) {
|
||||
AppBeaconServer.get().getCache().getShellSessions().add(new BeaconShellSession(ref.get(), control));
|
||||
}
|
||||
|
||||
return new BeaconShellSession(ref.get(), control);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package io.xpipe.app.beacon;
|
||||
|
||||
import io.xpipe.app.beacon.mcp.AppMcpServer;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.util.DocumentationLink;
|
||||
import io.xpipe.beacon.BeaconConfig;
|
||||
import io.xpipe.beacon.BeaconInterface;
|
||||
import io.xpipe.core.OsType;
|
||||
import io.xpipe.core.XPipeInstallation;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
@@ -33,19 +33,24 @@ public class AppBeaconServer {
|
||||
@Getter
|
||||
private final boolean propertyPort;
|
||||
|
||||
private boolean running;
|
||||
private ExecutorService executor;
|
||||
private HttpServer server;
|
||||
|
||||
@Getter
|
||||
private final Set<BeaconSession> sessions = new HashSet<>();
|
||||
|
||||
@Getter
|
||||
private final AppBeaconCache cache = new AppBeaconCache();
|
||||
|
||||
private boolean running;
|
||||
private ExecutorService executor;
|
||||
private HttpServer server;
|
||||
|
||||
@Getter
|
||||
private String localAuthSecret;
|
||||
|
||||
private AppBeaconServer(int port, boolean propertyPort) {
|
||||
this.port = port;
|
||||
this.propertyPort = propertyPort;
|
||||
}
|
||||
|
||||
public static void setupPort() {
|
||||
int port;
|
||||
boolean propertyPort;
|
||||
@@ -53,17 +58,12 @@ public class AppBeaconServer {
|
||||
port = BeaconConfig.getUsedPort();
|
||||
propertyPort = true;
|
||||
} else {
|
||||
port = XPipeInstallation.getDefaultBeaconPort();
|
||||
port = BeaconConfig.getDefaultBeaconPort();
|
||||
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();
|
||||
@@ -85,18 +85,25 @@ public class AppBeaconServer {
|
||||
if (INSTANCE != null) {
|
||||
INSTANCE.stop();
|
||||
INSTANCE.deleteAuthSecret();
|
||||
for (BeaconShellSession ss : INSTANCE.getCache().getShellSessions()) {
|
||||
try {
|
||||
ss.getControl().close();
|
||||
} catch (Exception ex) {
|
||||
ErrorEventFactory.fromThrowable(ex).omit().expected().handle();
|
||||
}
|
||||
}
|
||||
INSTANCE = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void addSession(BeaconSession session) {
|
||||
this.sessions.add(session);
|
||||
}
|
||||
|
||||
public static AppBeaconServer get() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public void addSession(BeaconSession session) {
|
||||
this.sessions.add(session);
|
||||
}
|
||||
|
||||
private void stop() {
|
||||
if (!running) {
|
||||
return;
|
||||
@@ -112,7 +119,7 @@ public class AppBeaconServer {
|
||||
}
|
||||
|
||||
private void initAuthSecret() throws IOException {
|
||||
var file = XPipeInstallation.getLocalBeaconAuthFile();
|
||||
var file = BeaconConfig.getLocalBeaconAuthFile();
|
||||
var id = UUID.randomUUID().toString();
|
||||
Files.writeString(file, id);
|
||||
if (OsType.getLocal() != OsType.WINDOWS) {
|
||||
@@ -122,7 +129,7 @@ public class AppBeaconServer {
|
||||
}
|
||||
|
||||
private void deleteAuthSecret() {
|
||||
var file = XPipeInstallation.getLocalBeaconAuthFile();
|
||||
var file = BeaconConfig.getLocalBeaconAuthFile();
|
||||
try {
|
||||
Files.delete(file);
|
||||
} catch (IOException ignored) {
|
||||
@@ -150,6 +157,13 @@ public class AppBeaconServer {
|
||||
handleCatchAll(exchange);
|
||||
});
|
||||
|
||||
server.createContext("/mcp", exchange -> {
|
||||
var mcpServer = AppMcpServer.get();
|
||||
if (mcpServer != null) {
|
||||
mcpServer.createHttpHandler().handle(exchange);
|
||||
}
|
||||
});
|
||||
|
||||
server.start();
|
||||
running = true;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package io.xpipe.app.beacon.impl;
|
||||
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.terminal.TerminalView;
|
||||
import io.xpipe.app.util.*;
|
||||
@@ -67,6 +66,11 @@ public class AskpassExchangeImpl extends AskpassExchange {
|
||||
return Response.builder().value(secret.inPlace()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresEnabledApi() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void focusTerminalIfNeeded(long pid) {
|
||||
if (TerminalView.get() == null) {
|
||||
return;
|
||||
@@ -85,9 +89,4 @@ public class AskpassExchangeImpl extends AskpassExchange {
|
||||
}
|
||||
TerminalView.focus(term.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresEnabledApi() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ public class DaemonModeExchangeImpl extends DaemonModeExchange {
|
||||
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {
|
||||
var mode = OperationMode.map(msg.getMode());
|
||||
if (!mode.isSupported()) {
|
||||
throw new BeaconClientException("Unsupported mode: " + msg.getMode().getDisplayName() + ". Supported: "
|
||||
throw new BeaconClientException("Unsupported mode: " + msg.getMode().getDisplayName()
|
||||
+ ". Supported: "
|
||||
+ String.join(
|
||||
", ",
|
||||
OperationMode.getAll().stream()
|
||||
@@ -21,9 +22,7 @@ public class DaemonModeExchangeImpl extends DaemonModeExchange {
|
||||
}
|
||||
|
||||
OperationMode.switchToSyncIfPossible(mode);
|
||||
return DaemonModeExchange.Response.builder()
|
||||
.usedMode(OperationMode.map(OperationMode.get()))
|
||||
.build();
|
||||
return DaemonModeExchange.Response.builder().usedMode(msg.getMode()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -13,6 +13,11 @@ public class DaemonOpenExchangeImpl extends DaemonOpenExchange {
|
||||
|
||||
private int openCounter = 0;
|
||||
|
||||
@Override
|
||||
public boolean requiresCompletedStartup() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object handle(HttpExchange exchange, Request msg) throws BeaconServerException {
|
||||
if (msg.getArguments().isEmpty()) {
|
||||
@@ -41,9 +46,4 @@ public class DaemonOpenExchangeImpl extends DaemonOpenExchange {
|
||||
public boolean requiresEnabledApi() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresCompletedStartup() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,7 @@ public class DaemonVersionExchangeImpl extends DaemonVersionExchange {
|
||||
|
||||
@Override
|
||||
public Object handle(HttpExchange exchange, Request msg) {
|
||||
var jvmVersion = System.getProperty("java.vm.vendor") + " "
|
||||
+ System.getProperty("java.vm.name") + " ("
|
||||
var jvmVersion = System.getProperty("java.vm.vendor") + " " + System.getProperty("java.vm.name") + " ("
|
||||
+ System.getProperty("java.vm.version") + ")";
|
||||
var version = AppProperties.get().getVersion();
|
||||
return Response.builder()
|
||||
|
||||
@@ -29,6 +29,11 @@ public class HandshakeExchangeImpl extends HandshakeExchange {
|
||||
return Response.builder().sessionToken(session.getToken()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresEnabledApi() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean checkAuth(BeaconAuthMethod authMethod) {
|
||||
if (authMethod instanceof BeaconAuthMethod.Local local) {
|
||||
var c = local.getAuthFileContent().strip();
|
||||
@@ -42,9 +47,4 @@ public class HandshakeExchangeImpl extends HandshakeExchange {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresEnabledApi() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+10
-10
@@ -44,6 +44,16 @@ public class TerminalExternalLaunchExchangeImpl extends TerminalExternalLaunchEx
|
||||
return Response.builder().command(r).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresEnabledApi() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getSynchronizationObject() {
|
||||
return DataStorage.get();
|
||||
}
|
||||
|
||||
private boolean checkPermission() {
|
||||
var cache = AppCache.getBoolean("externalLaunchPermitted", false);
|
||||
if (cache) {
|
||||
@@ -56,14 +66,4 @@ public class TerminalExternalLaunchExchangeImpl extends TerminalExternalLaunchEx
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresEnabledApi() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getSynchronizationObject() {
|
||||
return DataStorage.get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
package io.xpipe.app.beacon.mcp;
|
||||
|
||||
import io.xpipe.app.core.AppNames;
|
||||
import io.xpipe.app.core.AppProperties;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import io.modelcontextprotocol.server.McpServerFeatures;
|
||||
import io.modelcontextprotocol.server.McpSyncServer;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.Value;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@Value
|
||||
public class AppMcpServer {
|
||||
|
||||
private static AppMcpServer INSTANCE;
|
||||
McpSyncServer mcpSyncServer;
|
||||
HttpStreamableServerTransportProvider transportProvider;
|
||||
|
||||
public static AppMcpServer get() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static void init() {
|
||||
var transportProvider = new HttpStreamableServerTransportProvider(
|
||||
new ObjectMapper(), "/mcp", false, (req, context) -> context, null);
|
||||
|
||||
McpSyncServer syncServer = io.modelcontextprotocol.server.McpServer.sync(transportProvider)
|
||||
.serverInfo(AppNames.ofCurrent().getName(), AppProperties.get().getVersion())
|
||||
.capabilities(McpSchema.ServerCapabilities.builder()
|
||||
.resources(true, true)
|
||||
.tools(true)
|
||||
.prompts(false)
|
||||
.completions()
|
||||
.build())
|
||||
.build();
|
||||
|
||||
syncServer.addTool(McpTools.listSystems());
|
||||
syncServer.addTool(McpTools.readFile());
|
||||
syncServer.addTool(McpTools.listFiles());
|
||||
syncServer.addTool(McpTools.findFile());
|
||||
syncServer.addTool(McpTools.getFileInfo());
|
||||
|
||||
var mutationTools = new ArrayList<McpServerFeatures.SyncToolSpecification>();
|
||||
mutationTools.add(McpTools.createFile());
|
||||
mutationTools.add(McpTools.writeFile());
|
||||
mutationTools.add(McpTools.createDirectory());
|
||||
mutationTools.add(McpTools.runCommand());
|
||||
mutationTools.add(McpTools.runScript());
|
||||
mutationTools.add(McpTools.openTerminal());
|
||||
mutationTools.add(McpTools.openTerminalInline());
|
||||
mutationTools.add(McpTools.toggleState());
|
||||
|
||||
var toolsAdded = new AtomicBoolean();
|
||||
AppPrefs.get().enableMcpMutationTools().subscribe(value -> {
|
||||
for (var mutationTool : mutationTools) {
|
||||
if (value) {
|
||||
syncServer.addTool(mutationTool);
|
||||
} else if (toolsAdded.get()) {
|
||||
syncServer.removeTool(mutationTool.tool().name());
|
||||
}
|
||||
}
|
||||
if (value) {
|
||||
toolsAdded.set(true);
|
||||
}
|
||||
syncServer.notifyToolsListChanged();
|
||||
});
|
||||
|
||||
INSTANCE = new AppMcpServer(syncServer, transportProvider);
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
INSTANCE.mcpSyncServer.close();
|
||||
INSTANCE = null;
|
||||
}
|
||||
|
||||
public HttpHandler createHttpHandler() {
|
||||
return new HttpHandler() {
|
||||
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) throws IOException {
|
||||
try (exchange) {
|
||||
if (AppPrefs.get() == null) {
|
||||
transportProvider.sendError(exchange, 503, "Not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AppPrefs.get().enableMcpServer().get()) {
|
||||
transportProvider.sendError(exchange, 403, "MCP server is not enabled in the settings menu");
|
||||
if (exchange.getRequestMethod().equals("POST")) {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
ErrorEventFactory.fromMessage(
|
||||
"An external request was made to the XPipe MCP server, however the MCP server is not enabled in the"
|
||||
+ " settings menu")
|
||||
.expected()
|
||||
.handle();
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AppPrefs.get().disableApiAuthentication().get()) {
|
||||
var apiKey = exchange.getRequestHeaders().getFirst("Authorization");
|
||||
if (apiKey == null) {
|
||||
transportProvider.sendError(exchange, 403, "Header Authorization is not set");
|
||||
if (exchange.getRequestMethod().equals("POST")) {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
ErrorEventFactory.fromMessage(
|
||||
"An external request was made to the XPipe MCP server without the header Authorization set. "
|
||||
+ "Please configure your MCP client with the Bearer API token you can find the API "
|
||||
+ "settings menu")
|
||||
.expected()
|
||||
.handle();
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var correct = apiKey.replace("Bearer ", "")
|
||||
.equals(AppPrefs.get().apiKey().get());
|
||||
if (!correct) {
|
||||
transportProvider.sendError(exchange, 403, "Invalid API key");
|
||||
if (exchange.getRequestMethod().equals("POST")) {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
ErrorEventFactory.fromMessage(
|
||||
"The Authorization header sent by the MCP client is not correct")
|
||||
.expected()
|
||||
.handle();
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (exchange.getRequestMethod().equals("GET")) {
|
||||
transportProvider.doGet(exchange);
|
||||
} else if (exchange.getRequestMethod().equals("POST")) {
|
||||
transportProvider.doPost(exchange);
|
||||
} else if (exchange.getRequestMethod().equals("DELETE")) {
|
||||
transportProvider.doDelete(exchange);
|
||||
} else {
|
||||
transportProvider.doOther(exchange);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,533 @@
|
||||
/*
|
||||
* Copyright 2024-2024 the original author or authors.
|
||||
*/
|
||||
|
||||
package io.xpipe.app.beacon.mcp;
|
||||
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import io.modelcontextprotocol.server.DefaultMcpTransportContext;
|
||||
import io.modelcontextprotocol.server.McpTransportContext;
|
||||
import io.modelcontextprotocol.server.McpTransportContextExtractor;
|
||||
import io.modelcontextprotocol.spec.*;
|
||||
import io.modelcontextprotocol.util.Assert;
|
||||
import io.modelcontextprotocol.util.KeepAliveScheduler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public class HttpStreamableServerTransportProvider implements McpStreamableServerTransportProvider {
|
||||
|
||||
public static final String MESSAGE_EVENT_TYPE = "message";
|
||||
|
||||
public static final String ENDPOINT_EVENT_TYPE = "endpoint";
|
||||
|
||||
public static final String UTF_8 = "UTF-8";
|
||||
public static final String APPLICATION_JSON = "application/json";
|
||||
public static final String TEXT_EVENT_STREAM = "text/event-stream";
|
||||
public static final String FAILED_TO_SEND_ERROR_RESPONSE = "Failed to send error response: {}";
|
||||
private static final Logger logger = LoggerFactory.getLogger(HttpStreamableServerTransportProvider.class);
|
||||
|
||||
private static final String ACCEPT = "Accept";
|
||||
|
||||
private final String mcpEndpoint;
|
||||
|
||||
private final boolean disallowDelete;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private final ConcurrentHashMap<String, McpStreamableServerSession> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
private final McpTransportContextExtractor<HttpExchange> contextExtractor;
|
||||
private McpStreamableServerSession.Factory sessionFactory;
|
||||
|
||||
private volatile boolean isClosing = false;
|
||||
|
||||
private KeepAliveScheduler keepAliveScheduler;
|
||||
|
||||
HttpStreamableServerTransportProvider(
|
||||
ObjectMapper objectMapper,
|
||||
String mcpEndpoint,
|
||||
boolean disallowDelete,
|
||||
McpTransportContextExtractor<HttpExchange> contextExtractor,
|
||||
Duration keepAliveInterval) {
|
||||
Assert.notNull(objectMapper, "ObjectMapper must not be null");
|
||||
Assert.notNull(mcpEndpoint, "MCP endpoint must not be null");
|
||||
Assert.notNull(contextExtractor, "Context extractor must not be null");
|
||||
|
||||
this.objectMapper = objectMapper;
|
||||
this.mcpEndpoint = mcpEndpoint;
|
||||
this.disallowDelete = disallowDelete;
|
||||
this.contextExtractor = contextExtractor;
|
||||
|
||||
if (keepAliveInterval != null) {
|
||||
|
||||
this.keepAliveScheduler = KeepAliveScheduler.builder(
|
||||
() -> (isClosing) ? Flux.empty() : Flux.fromIterable(sessions.values()))
|
||||
.initialDelay(keepAliveInterval)
|
||||
.interval(keepAliveInterval)
|
||||
.build();
|
||||
|
||||
this.keepAliveScheduler.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String protocolVersion() {
|
||||
return "2025-03-26";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSessionFactory(McpStreamableServerSession.Factory sessionFactory) {
|
||||
this.sessionFactory = sessionFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> notifyClients(String method, Object params) {
|
||||
if (this.sessions.isEmpty()) {
|
||||
logger.debug("No active sessions to broadcast message to");
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
logger.debug("Attempting to broadcast message to {} active sessions", this.sessions.size());
|
||||
|
||||
return Mono.fromRunnable(() -> {
|
||||
this.sessions.values().parallelStream().forEach(session -> {
|
||||
try {
|
||||
session.sendNotification(method, params).block();
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to send message to session {}: {}", session.getId(), e.getMessage());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a graceful shutdown of the transport.
|
||||
*
|
||||
* @return A Mono that completes when all cleanup operations are finished
|
||||
*/
|
||||
@Override
|
||||
public Mono<Void> closeGracefully() {
|
||||
return Mono.fromRunnable(() -> {
|
||||
this.isClosing = true;
|
||||
logger.debug("Initiating graceful shutdown with {} active sessions", this.sessions.size());
|
||||
|
||||
this.sessions.values().parallelStream().forEach(session -> {
|
||||
try {
|
||||
session.closeGracefully().block();
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to close session {}: {}", session.getId(), e.getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
this.sessions.clear();
|
||||
logger.debug("Graceful shutdown completed");
|
||||
})
|
||||
.then()
|
||||
.doOnSuccess(v -> {
|
||||
sessions.clear();
|
||||
logger.debug("Graceful shutdown completed");
|
||||
if (this.keepAliveScheduler != null) {
|
||||
this.keepAliveScheduler.shutdown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void doGet(HttpExchange exchange) throws IOException {
|
||||
|
||||
String requestURI = exchange.getRequestURI().toString();
|
||||
if (!requestURI.endsWith(mcpEndpoint)) {
|
||||
sendError(exchange, 404, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isClosing) {
|
||||
sendError(exchange, 503, "Server is shutting down");
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> badRequestErrors = new ArrayList<>();
|
||||
|
||||
String accept = exchange.getRequestHeaders().getFirst(ACCEPT);
|
||||
if (accept == null || !accept.contains(TEXT_EVENT_STREAM)) {
|
||||
badRequestErrors.add("text/event-stream required in Accept header");
|
||||
}
|
||||
|
||||
String sessionId = exchange.getRequestHeaders().getFirst(HttpHeaders.MCP_SESSION_ID);
|
||||
|
||||
if (sessionId == null || sessionId.isBlank()) {
|
||||
badRequestErrors.add("Session ID required in mcp-session-id header");
|
||||
}
|
||||
|
||||
if (!badRequestErrors.isEmpty()) {
|
||||
String combinedMessage = String.join("; ", badRequestErrors);
|
||||
this.sendError(exchange, 400, combinedMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
McpStreamableServerSession session = this.sessions.get(sessionId);
|
||||
|
||||
if (session == null) {
|
||||
sendError(exchange, 404, null);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Handling GET request for session: {}", sessionId);
|
||||
|
||||
McpTransportContext transportContext =
|
||||
this.contextExtractor.extract(exchange, new DefaultMcpTransportContext());
|
||||
|
||||
try {
|
||||
exchange.getResponseHeaders().add("Content-Type", TEXT_EVENT_STREAM);
|
||||
exchange.getResponseHeaders().add("Content-Encoding", UTF_8);
|
||||
exchange.getResponseHeaders().add("Cache-Control", "no-cache");
|
||||
exchange.getResponseHeaders().add("Connection", "keep-alive");
|
||||
exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
|
||||
exchange.sendResponseHeaders(200, 0);
|
||||
|
||||
var writer = new PrintWriter(exchange.getResponseBody());
|
||||
HttpServletStreamableMcpSessionTransport sessionTransport =
|
||||
new HttpServletStreamableMcpSessionTransport(sessionId, exchange, writer);
|
||||
|
||||
// Check if this is a replay request
|
||||
if (exchange.getRequestHeaders().getFirst(HttpHeaders.LAST_EVENT_ID) != null) {
|
||||
String lastId = exchange.getRequestHeaders().getFirst(HttpHeaders.LAST_EVENT_ID);
|
||||
|
||||
try {
|
||||
session.replay(lastId)
|
||||
.contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext))
|
||||
.toIterable()
|
||||
.forEach(message -> {
|
||||
try {
|
||||
sessionTransport
|
||||
.sendMessage(message)
|
||||
.contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext))
|
||||
.block();
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to replay message: {}", e.getMessage());
|
||||
exchange.close();
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to replay messages: {}", e.getMessage());
|
||||
exchange.close();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to handle GET request for session {}: {}", sessionId, e.getMessage());
|
||||
sendError(exchange, 500, null);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendError(HttpExchange exchange, int code, String message) throws IOException {
|
||||
var b = message != null ? message.getBytes(StandardCharsets.UTF_8) : new byte[0];
|
||||
exchange.getResponseHeaders().add("Content-Encoding", UTF_8);
|
||||
exchange.sendResponseHeaders(code, b.length != 0 ? b.length : -1);
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
os.write(b);
|
||||
}
|
||||
|
||||
TrackEvent.error("MCP server error: " + message);
|
||||
}
|
||||
|
||||
public void doPost(HttpExchange exchange) throws IOException {
|
||||
|
||||
String requestURI = exchange.getRequestURI().toString();
|
||||
if (!requestURI.endsWith(mcpEndpoint)) {
|
||||
sendError(exchange, 404, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isClosing) {
|
||||
sendError(exchange, 503, "Server is shutting down");
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> badRequestErrors = new ArrayList<>();
|
||||
|
||||
String accept = exchange.getRequestHeaders().getFirst(ACCEPT);
|
||||
if (accept == null || !accept.contains(TEXT_EVENT_STREAM)) {
|
||||
badRequestErrors.add("text/event-stream required in Accept header");
|
||||
}
|
||||
if (accept == null || !accept.contains(APPLICATION_JSON)) {
|
||||
badRequestErrors.add("application/json required in Accept header");
|
||||
}
|
||||
|
||||
McpTransportContext transportContext =
|
||||
this.contextExtractor.extract(exchange, new DefaultMcpTransportContext());
|
||||
|
||||
try {
|
||||
var body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
|
||||
|
||||
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body);
|
||||
|
||||
// Handle initialization request
|
||||
if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest
|
||||
&& jsonrpcRequest.method().equals(McpSchema.METHOD_INITIALIZE)) {
|
||||
if (!badRequestErrors.isEmpty()) {
|
||||
String combinedMessage = String.join("; ", badRequestErrors);
|
||||
this.sendError(exchange, 400, combinedMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
McpSchema.InitializeRequest initializeRequest =
|
||||
objectMapper.convertValue(jsonrpcRequest.params(), new TypeReference<>() {});
|
||||
McpStreamableServerSession.McpStreamableServerSessionInit init =
|
||||
this.sessionFactory.startSession(initializeRequest);
|
||||
this.sessions.put(init.session().getId(), init.session());
|
||||
|
||||
try {
|
||||
McpSchema.InitializeResult initResult = init.initResult().block();
|
||||
|
||||
String jsonResponse = objectMapper.writeValueAsString(new McpSchema.JSONRPCResponse(
|
||||
McpSchema.JSONRPC_VERSION, jsonrpcRequest.id(), initResult, null));
|
||||
var jsonBytes = jsonResponse.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
exchange.getResponseHeaders().add("Content-Type", APPLICATION_JSON);
|
||||
exchange.getResponseHeaders().add("Content-Encoding", UTF_8);
|
||||
exchange.getResponseHeaders()
|
||||
.add(HttpHeaders.MCP_SESSION_ID, init.session().getId());
|
||||
exchange.sendResponseHeaders(200, jsonBytes.length);
|
||||
exchange.getResponseBody().write(jsonBytes);
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to initialize session: {}", e.getMessage());
|
||||
this.sendError(exchange, 500, "Failed to initialize session: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
String sessionId = exchange.getRequestHeaders().getFirst(HttpHeaders.MCP_SESSION_ID);
|
||||
|
||||
if (sessionId == null || sessionId.isBlank()) {
|
||||
badRequestErrors.add("Session ID required in mcp-session-id header");
|
||||
}
|
||||
|
||||
if (!badRequestErrors.isEmpty()) {
|
||||
String combinedMessage = String.join("; ", badRequestErrors);
|
||||
this.sendError(exchange, 400, combinedMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
McpStreamableServerSession session = this.sessions.get(sessionId);
|
||||
|
||||
if (session == null) {
|
||||
this.sendError(exchange, 404, "Session not found: " + sessionId + ". Was the session not refreshed?");
|
||||
return;
|
||||
}
|
||||
|
||||
if (message instanceof McpSchema.JSONRPCResponse jsonrpcResponse) {
|
||||
session.accept(jsonrpcResponse)
|
||||
.contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext))
|
||||
.block();
|
||||
exchange.sendResponseHeaders(200, -1);
|
||||
} else if (message instanceof McpSchema.JSONRPCNotification jsonrpcNotification) {
|
||||
session.accept(jsonrpcNotification)
|
||||
.contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext))
|
||||
.block();
|
||||
exchange.sendResponseHeaders(202, -1);
|
||||
} else if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) {
|
||||
// For streaming responses, we need to return SSE
|
||||
exchange.getResponseHeaders().add("Content-Type", TEXT_EVENT_STREAM);
|
||||
exchange.getResponseHeaders().add("Content-Encoding", UTF_8);
|
||||
exchange.getResponseHeaders().add("Cache-Control", "no-cache");
|
||||
exchange.getResponseHeaders().add("Connection", "keep-alive");
|
||||
exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
|
||||
exchange.sendResponseHeaders(200, 0);
|
||||
|
||||
var writer = new PrintWriter(exchange.getResponseBody());
|
||||
|
||||
HttpServletStreamableMcpSessionTransport sessionTransport =
|
||||
new HttpServletStreamableMcpSessionTransport(sessionId, exchange, writer);
|
||||
|
||||
try {
|
||||
session.responseStream(jsonrpcRequest, sessionTransport)
|
||||
.contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext))
|
||||
.block();
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to handle request stream: {}", e.getMessage());
|
||||
exchange.close();
|
||||
}
|
||||
} else {
|
||||
this.sendError(exchange, 500, "Unknown message type");
|
||||
}
|
||||
} catch (IllegalArgumentException | IOException e) {
|
||||
logger.error("Failed to deserialize message: {}", e.getMessage());
|
||||
this.sendError(exchange, 400, "Invalid message format: " + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
logger.error("Error handling message: {}", e.getMessage());
|
||||
try {
|
||||
this.sendError(exchange, 500, "Error processing message: " + e.getMessage());
|
||||
} catch (IOException ex) {
|
||||
logger.error(FAILED_TO_SEND_ERROR_RESPONSE, ex.getMessage());
|
||||
sendError(exchange, 500, "Error processing message");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void doOther(HttpExchange exchange) throws IOException {
|
||||
sendError(exchange, 405, "Unsupported HTTP method: " + exchange.getRequestMethod());
|
||||
}
|
||||
|
||||
protected void doDelete(HttpExchange exchange) throws IOException {
|
||||
|
||||
String requestURI = exchange.getRequestURI().toString();
|
||||
if (!requestURI.endsWith(mcpEndpoint)) {
|
||||
sendError(exchange, 404, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isClosing) {
|
||||
sendError(exchange, 503, "Server is shutting down");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.disallowDelete) {
|
||||
sendError(exchange, 405, null);
|
||||
return;
|
||||
}
|
||||
|
||||
McpTransportContext transportContext =
|
||||
this.contextExtractor.extract(exchange, new DefaultMcpTransportContext());
|
||||
|
||||
if (exchange.getRequestHeaders().getFirst(HttpHeaders.MCP_SESSION_ID) == null) {
|
||||
sendError(exchange, 400, "Session ID required in mcp-session-id header");
|
||||
return;
|
||||
}
|
||||
|
||||
String sessionId = exchange.getRequestHeaders().getFirst(HttpHeaders.MCP_SESSION_ID);
|
||||
McpStreamableServerSession session = this.sessions.get(sessionId);
|
||||
|
||||
if (session == null) {
|
||||
sendError(exchange, 404, null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
session.delete()
|
||||
.contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext))
|
||||
.block();
|
||||
this.sessions.remove(sessionId);
|
||||
exchange.sendResponseHeaders(200, -1);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to delete session {}: {}", sessionId, e.getMessage());
|
||||
try {
|
||||
sendError(exchange, 500, e.getMessage());
|
||||
} catch (IOException ex) {
|
||||
logger.error(FAILED_TO_SEND_ERROR_RESPONSE, ex.getMessage());
|
||||
sendError(exchange, 500, "Error deleting session");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sendEvent(PrintWriter writer, String eventType, String data, String id) throws IOException {
|
||||
if (id != null) {
|
||||
writer.write("id: " + id + "\n");
|
||||
}
|
||||
writer.write("event: " + eventType + "\n");
|
||||
writer.write("data: " + data + "\n\n");
|
||||
writer.flush();
|
||||
|
||||
if (writer.checkError()) {
|
||||
throw new IOException("Client disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
private class HttpServletStreamableMcpSessionTransport implements McpStreamableServerTransport {
|
||||
|
||||
private final String sessionId;
|
||||
|
||||
private final HttpExchange exchange;
|
||||
|
||||
private final PrintWriter writer;
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private volatile boolean closed = false;
|
||||
|
||||
HttpServletStreamableMcpSessionTransport(String sessionId, HttpExchange exchange, PrintWriter writer) {
|
||||
this.sessionId = sessionId;
|
||||
this.exchange = exchange;
|
||||
this.writer = writer;
|
||||
logger.debug("Streamable session transport {} initialized with SSE writer", sessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message, String messageId) {
|
||||
return Mono.fromRunnable(() -> {
|
||||
if (this.closed) {
|
||||
logger.debug("Attempted to send message to closed session: {}", this.sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
lock.lock();
|
||||
try {
|
||||
if (this.closed) {
|
||||
logger.debug("Session {} was closed during message send attempt", this.sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
String jsonText = objectMapper.writeValueAsString(message);
|
||||
HttpStreamableServerTransportProvider.this.sendEvent(
|
||||
writer, MESSAGE_EVENT_TYPE, jsonText, messageId != null ? messageId : this.sessionId);
|
||||
logger.debug("Message sent to session {} with ID {}", this.sessionId, messageId);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to send message to session {}: {}", this.sessionId, e.getMessage());
|
||||
HttpStreamableServerTransportProvider.this.sessions.remove(this.sessionId);
|
||||
exchange.close();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
lock.lock();
|
||||
try {
|
||||
if (this.closed) {
|
||||
logger.debug("Session transport {} already closed", this.sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
this.closed = true;
|
||||
|
||||
// HttpServletStreamableServerTransportProvider.this.sessions.remove(this.sessionId);
|
||||
exchange.close();
|
||||
logger.debug("Successfully completed async context for session {}", sessionId);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to complete async context for session {}: {}", sessionId, e.getMessage());
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> closeGracefully() {
|
||||
return Mono.fromRunnable(() -> {
|
||||
HttpServletStreamableMcpSessionTransport.this.close();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
|
||||
return sendMessage(message, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
|
||||
return objectMapper.convertValue(data, typeRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.xpipe.app.beacon.mcp;
|
||||
|
||||
import io.xpipe.core.JacksonMapper;
|
||||
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class McpSchemaFiles {
|
||||
|
||||
public static String load(String name) throws IOException {
|
||||
try (var in = McpSchemaFiles.class.getResourceAsStream("/io/xpipe/app/resources/mcp/" + name)) {
|
||||
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
public static McpSchema.Tool loadTool(String name) throws IOException {
|
||||
var s = load(name);
|
||||
return JacksonMapper.getDefault().readValue(s, McpSchema.Tool.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package io.xpipe.app.beacon.mcp;
|
||||
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStorageQuery;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.beacon.BeaconClientException;
|
||||
import io.xpipe.core.FilePath;
|
||||
|
||||
import io.modelcontextprotocol.server.McpSyncServerExchange;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
public interface McpToolHandler
|
||||
extends BiFunction<McpSyncServerExchange, McpSchema.CallToolRequest, McpSchema.CallToolResult> {
|
||||
|
||||
static McpToolHandler of(McpToolHandler t) {
|
||||
return t;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
default McpSchema.CallToolResult apply(
|
||||
McpSyncServerExchange mcpSyncServerExchange, McpSchema.CallToolRequest callToolRequest) {
|
||||
var req = new ToolRequest(mcpSyncServerExchange, callToolRequest);
|
||||
try {
|
||||
return handle(req);
|
||||
} catch (BeaconClientException e) {
|
||||
ErrorEventFactory.fromThrowable(e).expected().omit().handle();
|
||||
return McpSchema.CallToolResult.builder()
|
||||
.addTextContent(e.getMessage())
|
||||
.isError(true)
|
||||
.build();
|
||||
} catch (Throwable e) {
|
||||
ErrorEventFactory.fromThrowable(e).handle();
|
||||
return McpSchema.CallToolResult.builder()
|
||||
.addTextContent(e.getMessage())
|
||||
.isError(true)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
McpSchema.CallToolResult handle(ToolRequest request) throws Exception;
|
||||
|
||||
class ToolRequest {
|
||||
|
||||
protected final McpSyncServerExchange exchange;
|
||||
protected final McpSchema.CallToolRequest request;
|
||||
|
||||
public ToolRequest(McpSyncServerExchange exchange, McpSchema.CallToolRequest request) {
|
||||
this.exchange = exchange;
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
public McpSchema.CallToolRequest getRawRequest() {
|
||||
return request;
|
||||
}
|
||||
|
||||
public Optional<String> getOptionalStringArgument(String key) {
|
||||
var o = request.arguments().get(key);
|
||||
if (o == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (!(o instanceof String s) || s.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(s);
|
||||
}
|
||||
|
||||
public String getStringArgument(String key) throws BeaconClientException {
|
||||
var o = request.arguments().get(key);
|
||||
if (o == null) {
|
||||
throw new BeaconClientException("Missing argument for key " + key);
|
||||
}
|
||||
|
||||
if (!(o instanceof String s) || s.isBlank()) {
|
||||
throw new BeaconClientException("Invalid argument for key " + key);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
public Optional<Boolean> getOptionalBooleanArgument(String key) {
|
||||
var o = request.arguments().get(key);
|
||||
if (o == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (!(o instanceof Boolean b)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(b);
|
||||
}
|
||||
|
||||
public boolean getBooleanArgument(String key) throws BeaconClientException {
|
||||
var o = request.arguments().get(key);
|
||||
if (o == null) {
|
||||
throw new BeaconClientException("Missing argument for key " + key);
|
||||
}
|
||||
|
||||
if (!(o instanceof Boolean b)) {
|
||||
throw new BeaconClientException("Invalid argument for key " + key);
|
||||
}
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
public FilePath getFilePath(String key) throws BeaconClientException {
|
||||
var s = getStringArgument(key);
|
||||
var path = FilePath.parse(s);
|
||||
if (path == null) {
|
||||
throw new BeaconClientException("Invalid argument for key " + key);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
public DataStoreEntryRef<?> getDataStoreRef(String name) throws BeaconClientException {
|
||||
var found = DataStorageQuery.queryUserInput(name);
|
||||
if (found.isEmpty()) {
|
||||
throw new BeaconClientException("No connection found for input " + name);
|
||||
}
|
||||
|
||||
if (found.size() > 1) {
|
||||
throw new BeaconClientException("Multiple connections found: "
|
||||
+ found.stream().map(DataStoreEntry::getName).toList());
|
||||
}
|
||||
|
||||
var e = found.getFirst();
|
||||
return e.ref();
|
||||
}
|
||||
|
||||
public DataStoreEntryRef<ShellStore> getShellStoreRef(String name) throws BeaconClientException {
|
||||
var ref = getDataStoreRef(name);
|
||||
var isShell = ref.getStore() instanceof ShellStore;
|
||||
if (!isShell) {
|
||||
throw new BeaconClientException("Connection "
|
||||
+ DataStorage.get().getStorePath(ref.get()).toString() + " is not a shell connection");
|
||||
}
|
||||
|
||||
return ref.asNeeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
package io.xpipe.app.beacon.mcp;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import io.xpipe.app.beacon.AppBeaconServer;
|
||||
import io.xpipe.app.core.AppExtensionManager;
|
||||
import io.xpipe.app.core.AppNames;
|
||||
import io.xpipe.app.ext.ConnectionFileSystem;
|
||||
import io.xpipe.app.ext.FileEntry;
|
||||
import io.xpipe.app.ext.SingletonSessionStore;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.process.TerminalInitScriptConfig;
|
||||
import io.xpipe.app.process.WorkingDirectoryFunction;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStorageQuery;
|
||||
import io.xpipe.app.terminal.TerminalLaunch;
|
||||
import io.xpipe.app.util.CommandDialog;
|
||||
import io.xpipe.app.util.ScriptHelper;
|
||||
import io.xpipe.beacon.BeaconClientException;
|
||||
import io.xpipe.core.FileInfo;
|
||||
import io.xpipe.core.FilePath;
|
||||
import io.xpipe.core.JacksonMapper;
|
||||
|
||||
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
||||
import io.modelcontextprotocol.server.McpServerFeatures;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.xpipe.core.StorePath;
|
||||
import lombok.Builder;
|
||||
import lombok.NonNull;
|
||||
import lombok.Value;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
import org.apache.commons.lang3.ClassUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public final class McpTools {
|
||||
|
||||
@Jacksonized
|
||||
@Builder
|
||||
@Value
|
||||
public static class ConnectionResource {
|
||||
@NonNull
|
||||
String name;
|
||||
|
||||
@NonNull
|
||||
String path;
|
||||
}
|
||||
|
||||
public static McpServerFeatures.SyncToolSpecification listSystems() throws IOException {
|
||||
var tool = McpSchemaFiles.loadTool("list_systems.json");
|
||||
return McpServerFeatures.SyncToolSpecification.builder()
|
||||
.tool(tool)
|
||||
.callHandler(McpToolHandler.of((req) -> {
|
||||
var filter = req.getOptionalStringArgument("filter");
|
||||
var entries = filter.isPresent() ? DataStorageQuery.queryUserInput(filter.get()) : DataStorage.get().getStoreEntries();
|
||||
|
||||
var list = new ArrayList<ConnectionResource>();
|
||||
for (var e : entries) {
|
||||
if (!e.getValidity().isUsable()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var r = ConnectionResource.builder()
|
||||
.name(e.getName())
|
||||
.path(DataStorage.get().getStorePath(e).toString())
|
||||
.build();
|
||||
list.add(r);
|
||||
}
|
||||
|
||||
var json = JsonNodeFactory.instance.arrayNode();
|
||||
for (var e : list) {
|
||||
json.add(JacksonMapper.getDefault().valueToTree(e));
|
||||
}
|
||||
|
||||
var object = JsonNodeFactory.instance.objectNode();
|
||||
object.set("found", json);
|
||||
|
||||
return McpSchema.CallToolResult.builder()
|
||||
.structuredContent(JacksonMapper.getDefault().writeValueAsString(object))
|
||||
.build();
|
||||
}))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static McpServerFeatures.SyncToolSpecification readFile() throws IOException {
|
||||
var tool = McpSchemaFiles.loadTool("read_file.json");
|
||||
return McpServerFeatures.SyncToolSpecification.builder()
|
||||
.tool(tool)
|
||||
.callHandler(McpToolHandler.of((req) -> {
|
||||
var path = req.getFilePath("path");
|
||||
var system = req.getStringArgument("system");
|
||||
var shellStore = req.getShellStoreRef(system);
|
||||
var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);
|
||||
var fs = new ConnectionFileSystem(shellSession.getControl());
|
||||
|
||||
if (!fs.fileExists(path)) {
|
||||
throw new BeaconClientException("File " + path + " does not exist");
|
||||
}
|
||||
|
||||
try (var in = fs.openInput(path)) {
|
||||
var b = in.readAllBytes();
|
||||
var s = new String(b, StandardCharsets.UTF_8);
|
||||
return McpSchema.CallToolResult.builder()
|
||||
.addTextContent(s)
|
||||
.build();
|
||||
}
|
||||
}))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static McpServerFeatures.SyncToolSpecification listFiles() throws IOException {
|
||||
var tool = McpSchemaFiles.loadTool("list_files.json");
|
||||
return McpServerFeatures.SyncToolSpecification.builder()
|
||||
.tool(tool)
|
||||
.callHandler(McpToolHandler.of((req) -> {
|
||||
var path = req.getFilePath("path");
|
||||
var system = req.getStringArgument("system");
|
||||
var recursive = req.getOptionalBooleanArgument("recursive").orElse(false);
|
||||
var shellStore = req.getShellStoreRef(system);
|
||||
var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);
|
||||
var fs = new ConnectionFileSystem(shellSession.getControl());
|
||||
|
||||
if (!fs.directoryExists(path)) {
|
||||
throw new BeaconClientException("Directory " + path + " does not exist");
|
||||
}
|
||||
|
||||
try (var stream = recursive ? fs.listFilesRecursively(fs, path) : fs.listFiles(fs, path)) {
|
||||
var list = stream.toList();
|
||||
var builder = McpSchema.CallToolResult.builder();
|
||||
for (FileEntry e : list) {
|
||||
builder.addTextContent(e.getPath().toString());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
}))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static McpServerFeatures.SyncToolSpecification findFile() throws IOException {
|
||||
var tool = McpSchemaFiles.loadTool("find_file.json");
|
||||
return McpServerFeatures.SyncToolSpecification.builder()
|
||||
.tool(tool)
|
||||
.callHandler(McpToolHandler.of((req) -> {
|
||||
var path = req.getFilePath("path");
|
||||
var system = req.getStringArgument("system");
|
||||
var recursive = req.getOptionalBooleanArgument("recursive").orElse(false);
|
||||
var pattern = req.getStringArgument("name");
|
||||
var shellStore = req.getShellStoreRef(system);
|
||||
var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);
|
||||
var fs = new ConnectionFileSystem(shellSession.getControl());
|
||||
|
||||
if (!fs.directoryExists(path)) {
|
||||
throw new BeaconClientException("Directory " + path + " does not exist");
|
||||
}
|
||||
|
||||
var regex = Pattern.compile(DataStorageQuery.toRegex(pattern));
|
||||
try (var stream = recursive ? fs.listFilesRecursively(fs, path) : fs.listFiles(fs, path)) {
|
||||
var list = stream.toList();
|
||||
var builder = McpSchema.CallToolResult.builder();
|
||||
list.stream()
|
||||
.filter(fileEntry -> regex.matcher(
|
||||
fileEntry.getPath().toString())
|
||||
.find())
|
||||
.forEach(fileEntry -> {
|
||||
builder.addTextContent(fileEntry.getPath().toString());
|
||||
});
|
||||
return builder.build();
|
||||
}
|
||||
}))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static McpServerFeatures.SyncToolSpecification getFileInfo() throws IOException {
|
||||
var tool = McpSchemaFiles.loadTool("get_file_info.json");
|
||||
return McpServerFeatures.SyncToolSpecification.builder()
|
||||
.tool(tool)
|
||||
.callHandler(McpToolHandler.of((req) -> {
|
||||
var path = req.getFilePath("path");
|
||||
var system = req.getStringArgument("system");
|
||||
var shellStore = req.getShellStoreRef(system);
|
||||
var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);
|
||||
var fs = new ConnectionFileSystem(shellSession.getControl());
|
||||
|
||||
if (!fs.fileExists(path)) {
|
||||
throw new BeaconClientException("File " + path + " does not exist");
|
||||
}
|
||||
|
||||
var entry = fs.getFileInfo(path);
|
||||
if (entry.isEmpty()) {
|
||||
throw new BeaconClientException("File " + path + " does not exist");
|
||||
}
|
||||
|
||||
var map = new LinkedHashMap<String, Object>();
|
||||
map.put("path", entry.get().getPath().toString());
|
||||
map.put("size", entry.get().getSize());
|
||||
if (entry.get().getInfo() instanceof FileInfo.Unix u) {
|
||||
map.put("permissions", u.getPermissions());
|
||||
map.put("user", u.getUser());
|
||||
map.put("group", u.getGroup());
|
||||
} else if (entry.get().getInfo() instanceof FileInfo.Windows w) {
|
||||
map.put("attributes", w.getAttributes());
|
||||
}
|
||||
map.put("type", entry.get().getKind().toString().toLowerCase());
|
||||
map.put("date", entry.get().getDate().toString());
|
||||
map.entrySet().removeIf(e -> e.getValue() == null);
|
||||
|
||||
return McpSchema.CallToolResult.builder()
|
||||
.structuredContent(map)
|
||||
.build();
|
||||
}))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static McpServerFeatures.SyncToolSpecification createFile() throws IOException {
|
||||
var tool = McpSchemaFiles.loadTool("create_file.json");
|
||||
return McpServerFeatures.SyncToolSpecification.builder()
|
||||
.tool(tool)
|
||||
.callHandler(McpToolHandler.of((req) -> {
|
||||
var path = req.getFilePath("path");
|
||||
var system = req.getStringArgument("system");
|
||||
var shellStore = req.getShellStoreRef(system);
|
||||
var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);
|
||||
var fs = new ConnectionFileSystem(shellSession.getControl());
|
||||
|
||||
if (fs.fileExists(path)) {
|
||||
throw new BeaconClientException("File " + path + " does already exist");
|
||||
}
|
||||
|
||||
fs.touch(path);
|
||||
|
||||
if (req.getRawRequest().arguments().containsKey("content")) {
|
||||
var s = req.getRawRequest().arguments().get("content").toString();
|
||||
var b = s.getBytes(StandardCharsets.UTF_8);
|
||||
try (var out = fs.openOutput(path, b.length)) {
|
||||
out.write(b);
|
||||
}
|
||||
}
|
||||
|
||||
return McpSchema.CallToolResult.builder()
|
||||
.addTextContent("File created successfully")
|
||||
.build();
|
||||
}))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static McpServerFeatures.SyncToolSpecification writeFile() throws IOException {
|
||||
var tool = McpSchemaFiles.loadTool("write_file.json");
|
||||
return McpServerFeatures.SyncToolSpecification.builder()
|
||||
.tool(tool)
|
||||
.callHandler(McpToolHandler.of((req) -> {
|
||||
var path = req.getFilePath("path");
|
||||
var system = req.getStringArgument("system");
|
||||
var content = req.getStringArgument("content");
|
||||
var shellStore = req.getShellStoreRef(system);
|
||||
var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);
|
||||
var fs = new ConnectionFileSystem(shellSession.getControl());
|
||||
|
||||
var b = content.getBytes(StandardCharsets.UTF_8);
|
||||
try (var out = fs.openOutput(path, b.length)) {
|
||||
out.write(b);
|
||||
}
|
||||
|
||||
return McpSchema.CallToolResult.builder()
|
||||
.addTextContent("File written successfully")
|
||||
.build();
|
||||
}))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static McpServerFeatures.SyncToolSpecification createDirectory() throws IOException {
|
||||
var tool = McpSchemaFiles.loadTool("create_directory.json");
|
||||
return McpServerFeatures.SyncToolSpecification.builder()
|
||||
.tool(tool)
|
||||
.callHandler(McpToolHandler.of((req) -> {
|
||||
var path = req.getFilePath("path");
|
||||
var system = req.getStringArgument("system");
|
||||
var shellStore = req.getShellStoreRef(system);
|
||||
var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);
|
||||
var fs = new ConnectionFileSystem(shellSession.getControl());
|
||||
|
||||
if (fs.fileExists(path)) {
|
||||
throw new BeaconClientException("Directory " + path + " does already exist");
|
||||
}
|
||||
|
||||
fs.mkdirs(path);
|
||||
|
||||
return McpSchema.CallToolResult.builder()
|
||||
.addTextContent("Directory created successfully")
|
||||
.build();
|
||||
}))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static McpServerFeatures.SyncToolSpecification runCommand() throws IOException {
|
||||
var tool = McpSchemaFiles.loadTool("run_command.json");
|
||||
return McpServerFeatures.SyncToolSpecification.builder()
|
||||
.tool(tool)
|
||||
.callHandler(McpToolHandler.of((req) -> {
|
||||
var command = req.getStringArgument("command");
|
||||
var system = req.getStringArgument("system");
|
||||
var shellStore = req.getShellStoreRef(system);
|
||||
var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);
|
||||
|
||||
var out = shellSession.getControl().command(command).readStdoutOrThrow();
|
||||
var formatted = CommandDialog.formatOutput(out);
|
||||
|
||||
return McpSchema.CallToolResult.builder()
|
||||
.addTextContent(formatted)
|
||||
.build();
|
||||
}))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static McpServerFeatures.SyncToolSpecification runScript() throws IOException {
|
||||
var tool = McpSchemaFiles.loadTool("run_script.json");
|
||||
return McpServerFeatures.SyncToolSpecification.builder()
|
||||
.tool(tool)
|
||||
.callHandler(McpToolHandler.of((req) -> {
|
||||
var system = req.getStringArgument("system");
|
||||
var script = req.getDataStoreRef("script");
|
||||
var directory = req.getFilePath("directory");
|
||||
var arguments = req.getStringArgument("arguments");
|
||||
|
||||
var shellStore = req.getShellStoreRef(system);
|
||||
var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);
|
||||
|
||||
var clazz = Class.forName(
|
||||
AppExtensionManager.getInstance()
|
||||
.getExtendedLayer()
|
||||
.findModule(AppNames.extModuleName("base"))
|
||||
.orElseThrow(),
|
||||
AppNames.extModuleName("base") + ".script.SimpleScriptStore");
|
||||
var method = clazz.getDeclaredMethod("assembleScriptChain", ShellControl.class);
|
||||
var command = (String) method.invoke(script.getStore(), shellSession.getControl());
|
||||
var scriptFile = ScriptHelper.createExecScript(shellSession.getControl(), command);
|
||||
var out = shellSession
|
||||
.getControl()
|
||||
.command(shellSession
|
||||
.getControl()
|
||||
.getShellDialect()
|
||||
.runScriptCommand(shellSession.getControl(), scriptFile.toString())
|
||||
+ arguments)
|
||||
.withWorkingDirectory(directory)
|
||||
.readStdoutOrThrow();
|
||||
var formatted = CommandDialog.formatOutput(out);
|
||||
|
||||
return McpSchema.CallToolResult.builder()
|
||||
.addTextContent(formatted)
|
||||
.build();
|
||||
}))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static McpServerFeatures.SyncToolSpecification openTerminal() throws IOException {
|
||||
var tool = McpSchemaFiles.loadTool("open_terminal.json");
|
||||
return McpServerFeatures.SyncToolSpecification.builder()
|
||||
.tool(tool)
|
||||
.callHandler(McpToolHandler.of((req) -> {
|
||||
var system = req.getStringArgument("system");
|
||||
var directory = req.getOptionalStringArgument("directory");
|
||||
var shellStore = req.getShellStoreRef(system);
|
||||
var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);
|
||||
|
||||
TerminalLaunch.builder()
|
||||
.entry(shellStore.get())
|
||||
.directory(FilePath.of(directory.orElse(null)))
|
||||
.command(shellSession.getControl())
|
||||
.launch();
|
||||
|
||||
return McpSchema.CallToolResult.builder()
|
||||
.addTextContent("Terminal is launching")
|
||||
.build();
|
||||
}))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static McpServerFeatures.SyncToolSpecification openTerminalInline() throws IOException {
|
||||
var tool = McpSchemaFiles.loadTool("open_terminal_inline.json");
|
||||
return McpServerFeatures.SyncToolSpecification.builder()
|
||||
.tool(tool)
|
||||
.callHandler(McpToolHandler.of((req) -> {
|
||||
var system = req.getStringArgument("system");
|
||||
var directory = req.getOptionalStringArgument("directory");
|
||||
var shellStore = req.getShellStoreRef(system);
|
||||
var shellSession = AppBeaconServer.get().getCache().getOrStart(shellStore);
|
||||
|
||||
var script = shellSession
|
||||
.getControl()
|
||||
.prepareTerminalOpen(
|
||||
TerminalInitScriptConfig.ofName(
|
||||
shellStore.get().getName()),
|
||||
directory.isPresent()
|
||||
? WorkingDirectoryFunction.fixed(FilePath.parse(directory.get()))
|
||||
: WorkingDirectoryFunction.none());
|
||||
|
||||
var json = JsonNodeFactory.instance.objectNode();
|
||||
json.put("command", script);
|
||||
return McpSchema.CallToolResult.builder()
|
||||
.structuredContent(JacksonMapper.getDefault().writeValueAsString(json))
|
||||
.build();
|
||||
}))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static McpServerFeatures.SyncToolSpecification toggleState() throws IOException {
|
||||
var tool = McpSchemaFiles.loadTool("toggle_state.json");
|
||||
return McpServerFeatures.SyncToolSpecification.builder()
|
||||
.tool(tool)
|
||||
.callHandler(McpToolHandler.of((req) -> {
|
||||
var system = req.getStringArgument("system");
|
||||
var state = req.getBooleanArgument("state");
|
||||
var ref = req.getDataStoreRef(system);
|
||||
|
||||
if (!(ref.getStore() instanceof SingletonSessionStore<?> singletonSessionStore)) {
|
||||
throw new BeaconClientException("Not a toggleable connection");
|
||||
}
|
||||
if (state) {
|
||||
singletonSessionStore.startSessionIfNeeded();
|
||||
} else {
|
||||
singletonSessionStore.stopSessionIfNeeded();
|
||||
}
|
||||
|
||||
return McpSchema.CallToolResult.builder()
|
||||
.addTextContent("Connection state set to " + state)
|
||||
.build();
|
||||
}))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,8 @@ public class BrowserAbstractSessionModel<T extends BrowserSessionTab> {
|
||||
}
|
||||
|
||||
public void openSync(T e, BooleanProperty externalBusy) throws Exception {
|
||||
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
|
||||
try (var ignored =
|
||||
new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
|
||||
e.init();
|
||||
// Prevent multiple calls from interfering with each other
|
||||
synchronized (this) {
|
||||
|
||||
@@ -89,7 +89,8 @@ public class BrowserFileChooserSessionModel extends BrowserAbstractSessionModel<
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
BrowserFileSystemTabModel model;
|
||||
|
||||
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
|
||||
try (var ignored =
|
||||
new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
|
||||
model = new BrowserFileSystemTabModel(this, store, selectionMode);
|
||||
model.init();
|
||||
// Prevent multiple calls from interfering with each other
|
||||
|
||||
@@ -32,6 +32,30 @@ import java.util.*;
|
||||
public class BrowserFullSessionModel extends BrowserAbstractSessionModel<BrowserSessionTab> {
|
||||
|
||||
public static final BrowserFullSessionModel DEFAULT = new BrowserFullSessionModel();
|
||||
private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this);
|
||||
private final Property<Boolean> draggingFiles = new SimpleBooleanProperty();
|
||||
private final Property<BrowserSessionTab> globalPinnedTab = new SimpleObjectProperty<>();
|
||||
private final ObservableMap<BrowserSessionTab, BrowserSessionTab> splits = FXCollections.observableHashMap();
|
||||
private final ObservableValue<BrowserSessionTab> effectiveRightTab = createEffectiveRightTab();
|
||||
private final SequencedSet<BrowserSessionTab> previousTabs = new LinkedHashSet<>();
|
||||
|
||||
public BrowserFullSessionModel() {
|
||||
sessionEntries.addListener((ListChangeListener<? super BrowserSessionTab>) c -> {
|
||||
var v = globalPinnedTab.getValue();
|
||||
if (v != null && !c.getList().contains(v)) {
|
||||
globalPinnedTab.setValue(null);
|
||||
}
|
||||
|
||||
splits.keySet().removeIf(browserSessionTab -> !c.getList().contains(browserSessionTab));
|
||||
});
|
||||
|
||||
selectedEntry.addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
previousTabs.remove(newValue);
|
||||
previousTabs.add(newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void init() throws Exception {
|
||||
DEFAULT.openSync(new BrowserHistoryTabModel(DEFAULT), null);
|
||||
@@ -48,13 +72,6 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
|
||||
}
|
||||
}
|
||||
|
||||
private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this);
|
||||
private final Property<Boolean> draggingFiles = new SimpleBooleanProperty();
|
||||
private final Property<BrowserSessionTab> globalPinnedTab = new SimpleObjectProperty<>();
|
||||
private final ObservableMap<BrowserSessionTab, BrowserSessionTab> splits = FXCollections.observableHashMap();
|
||||
private final ObservableValue<BrowserSessionTab> effectiveRightTab = createEffectiveRightTab();
|
||||
private final SequencedSet<BrowserSessionTab> previousTabs = new LinkedHashSet<>();
|
||||
|
||||
private ObservableValue<BrowserSessionTab> createEffectiveRightTab() {
|
||||
return Bindings.createObjectBinding(
|
||||
() -> {
|
||||
@@ -88,24 +105,6 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
|
||||
splits);
|
||||
}
|
||||
|
||||
public BrowserFullSessionModel() {
|
||||
sessionEntries.addListener((ListChangeListener<? super BrowserSessionTab>) c -> {
|
||||
var v = globalPinnedTab.getValue();
|
||||
if (v != null && !c.getList().contains(v)) {
|
||||
globalPinnedTab.setValue(null);
|
||||
}
|
||||
|
||||
splits.keySet().removeIf(browserSessionTab -> !c.getList().contains(browserSessionTab));
|
||||
});
|
||||
|
||||
selectedEntry.addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
previousTabs.remove(newValue);
|
||||
previousTabs.add(newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Set<BrowserSessionTab> getAllTabs() {
|
||||
var set = new HashSet<BrowserSessionTab>();
|
||||
set.addAll(sessionEntries);
|
||||
@@ -221,8 +220,9 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
|
||||
boolean select)
|
||||
throws Exception {
|
||||
BrowserFileSystemTabModel model;
|
||||
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
|
||||
try (var sessionBusy = new BooleanScope(busy).exclusive().start()) {
|
||||
try (var ignored =
|
||||
new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
|
||||
try (var ignored2 = new BooleanScope(busy).exclusive().start()) {
|
||||
model = new BrowserFileSystemTabModel(this, store, BrowserFileSystemTabModel.SelectionMode.ALL);
|
||||
model.init();
|
||||
// Prevent multiple calls from interfering with each other
|
||||
|
||||
@@ -56,6 +56,54 @@ public class BrowserSessionTabsComp extends SimpleComp {
|
||||
this.headerHeight = new SimpleDoubleProperty();
|
||||
}
|
||||
|
||||
private static void setupKeyEvents(TabPane tabs) {
|
||||
tabs.addEventHandler(KeyEvent.KEY_PRESSED, keyEvent -> {
|
||||
var current = tabs.getSelectionModel().getSelectedItem();
|
||||
if (current == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(keyEvent)) {
|
||||
tabs.getTabs().remove(current);
|
||||
keyEvent.consume();
|
||||
return;
|
||||
}
|
||||
|
||||
if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN)
|
||||
.match(keyEvent)) {
|
||||
tabs.getTabs().clear();
|
||||
keyEvent.consume();
|
||||
}
|
||||
|
||||
if (keyEvent.getCode().isFunctionKey()) {
|
||||
var start = KeyCode.F1.getCode();
|
||||
var index = keyEvent.getCode().getCode() - start;
|
||||
if (index < tabs.getTabs().size()) {
|
||||
tabs.getSelectionModel().select(index);
|
||||
keyEvent.consume();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var forward = new KeyCodeCombination(KeyCode.TAB, KeyCombination.SHORTCUT_DOWN);
|
||||
if (forward.match(keyEvent)) {
|
||||
var next = (tabs.getSelectionModel().getSelectedIndex() + 1)
|
||||
% tabs.getTabs().size();
|
||||
tabs.getSelectionModel().select(next);
|
||||
keyEvent.consume();
|
||||
return;
|
||||
}
|
||||
|
||||
var back = new KeyCodeCombination(KeyCode.TAB, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN);
|
||||
if (back.match(keyEvent)) {
|
||||
var previous = (tabs.getTabs().size() + tabs.getSelectionModel().getSelectedIndex() - 1)
|
||||
% tabs.getTabs().size();
|
||||
tabs.getSelectionModel().select(previous);
|
||||
keyEvent.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Region createSimple() {
|
||||
var tabs = createTabPane();
|
||||
var topBackground = Comp.hspacer().styleClass("top-spacer").createRegion();
|
||||
@@ -246,54 +294,6 @@ public class BrowserSessionTabsComp extends SimpleComp {
|
||||
});
|
||||
}
|
||||
|
||||
private static void setupKeyEvents(TabPane tabs) {
|
||||
tabs.addEventHandler(KeyEvent.KEY_PRESSED, keyEvent -> {
|
||||
var current = tabs.getSelectionModel().getSelectedItem();
|
||||
if (current == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(keyEvent)) {
|
||||
tabs.getTabs().remove(current);
|
||||
keyEvent.consume();
|
||||
return;
|
||||
}
|
||||
|
||||
if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN)
|
||||
.match(keyEvent)) {
|
||||
tabs.getTabs().clear();
|
||||
keyEvent.consume();
|
||||
}
|
||||
|
||||
if (keyEvent.getCode().isFunctionKey()) {
|
||||
var start = KeyCode.F1.getCode();
|
||||
var index = keyEvent.getCode().getCode() - start;
|
||||
if (index < tabs.getTabs().size()) {
|
||||
tabs.getSelectionModel().select(index);
|
||||
keyEvent.consume();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var forward = new KeyCodeCombination(KeyCode.TAB, KeyCombination.SHORTCUT_DOWN);
|
||||
if (forward.match(keyEvent)) {
|
||||
var next = (tabs.getSelectionModel().getSelectedIndex() + 1)
|
||||
% tabs.getTabs().size();
|
||||
tabs.getSelectionModel().select(next);
|
||||
keyEvent.consume();
|
||||
return;
|
||||
}
|
||||
|
||||
var back = new KeyCodeCombination(KeyCode.TAB, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN);
|
||||
if (back.match(keyEvent)) {
|
||||
var previous = (tabs.getTabs().size() + tabs.getSelectionModel().getSelectedIndex() - 1)
|
||||
% tabs.getTabs().size();
|
||||
tabs.getSelectionModel().select(previous);
|
||||
keyEvent.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ContextMenu createContextMenu(TabPane tabs, Tab tab, BrowserSessionTab tabModel) {
|
||||
var cm = ContextMenuHelper.create();
|
||||
|
||||
|
||||
@@ -23,11 +23,6 @@ public abstract class BrowserStoreSessionTab<T extends DataStore> extends Browse
|
||||
this.name = DataStorage.get().getStoreEntryDisplayName(entry.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName() {
|
||||
return new SimpleStringProperty(name);
|
||||
}
|
||||
|
||||
public abstract Comp<?> comp();
|
||||
|
||||
public abstract boolean canImmediatelyClose();
|
||||
@@ -36,6 +31,11 @@ public abstract class BrowserStoreSessionTab<T extends DataStore> extends Browse
|
||||
|
||||
public abstract void close();
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName() {
|
||||
return new SimpleStringProperty(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIcon() {
|
||||
return entry.get().getEffectiveIconFile();
|
||||
|
||||
@@ -62,6 +62,11 @@ public abstract class BrowserAction extends StoreAction<FileSystemStore> {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterExecute() {
|
||||
model.getBusy().set(false);
|
||||
}
|
||||
|
||||
private void validateAutomatedAction() throws Exception {
|
||||
var bap = (BrowserActionProvider) getProvider();
|
||||
if (!bap.isApplicable(getModel(), getEntries())) {
|
||||
@@ -93,11 +98,6 @@ public abstract class BrowserAction extends StoreAction<FileSystemStore> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterExecute() {
|
||||
model.getBusy().set(false);
|
||||
}
|
||||
|
||||
public List<BrowserEntry> getEntries() {
|
||||
if (entries != null) {
|
||||
return entries;
|
||||
|
||||
@@ -32,11 +32,6 @@ public class ApplyFileEditActionProvider implements ActionProvider {
|
||||
@NonNull
|
||||
BrowserFileOutput output;
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
output.beforeTransfer();
|
||||
@@ -46,6 +41,11 @@ public class ApplyFileEditActionProvider implements ActionProvider {
|
||||
output.onFinish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> toDisplayMap() {
|
||||
var map = new LinkedHashMap<String, String>();
|
||||
|
||||
+14
-14
@@ -15,6 +15,20 @@ import java.util.List;
|
||||
|
||||
public class BrowseInNativeManagerActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return model.getFileSystem()
|
||||
.getShell()
|
||||
.orElseThrow()
|
||||
.getLocalSystemAccess()
|
||||
.supportsFileSystemAccess();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "browseInNativeFileManager";
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
@@ -32,18 +46,4 @@ public class BrowseInNativeManagerActionProvider implements BrowserActionProvide
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return model.getFileSystem()
|
||||
.getShell()
|
||||
.orElseThrow()
|
||||
.getLocalSystemAccess()
|
||||
.supportsFileSystemAccess();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "browseInNativeFileManager";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,11 +35,6 @@ public class ChgrpActionProvider implements BrowserActionProvider {
|
||||
|
||||
private final boolean recursive;
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
model.getFileSystem()
|
||||
@@ -57,5 +52,10 @@ public class ChgrpActionProvider implements BrowserActionProvider {
|
||||
.toList()));
|
||||
model.refreshBrowserEntriesSync(getEntries());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,11 +34,6 @@ public class ChmodActionProvider implements BrowserActionProvider {
|
||||
|
||||
private final boolean recursive;
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
model.getFileSystem()
|
||||
@@ -56,5 +51,10 @@ public class ChmodActionProvider implements BrowserActionProvider {
|
||||
.toList()));
|
||||
model.refreshBrowserEntriesSync(getEntries());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,11 +35,6 @@ public class ChownActionProvider implements BrowserActionProvider {
|
||||
|
||||
private final boolean recursive;
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
model.getFileSystem()
|
||||
@@ -57,5 +52,10 @@ public class ChownActionProvider implements BrowserActionProvider {
|
||||
.toList()));
|
||||
model.refreshBrowserEntriesSync(getEntries());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -10,6 +10,11 @@ import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
public class ComputeDirectorySizesActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "computeDirectorySizes";
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
@@ -34,9 +39,4 @@ public class ComputeDirectorySizesActionProvider implements BrowserActionProvide
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "computeDirectorySizes";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,15 +9,15 @@ import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
public class DeleteActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "deleteFile";
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeImpl() {
|
||||
var toDelete =
|
||||
@@ -25,10 +25,10 @@ public class DeleteActionProvider implements BrowserActionProvider {
|
||||
BrowserFileSystemHelper.delete(toDelete);
|
||||
model.refreshSync();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "deleteFile";
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@ import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
public class MoveFileActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "moveFile";
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
@@ -17,20 +22,15 @@ public class MoveFileActionProvider implements BrowserActionProvider {
|
||||
@NonNull
|
||||
FilePath target;
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
model.getFileSystem().move(getEntries().getFirst().getRawFileEntry().getPath(), target);
|
||||
model.refreshSync();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "moveFile";
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
public class NewDirectoryActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "newDirectory";
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
@@ -18,11 +23,6 @@ public class NewDirectoryActionProvider implements BrowserActionProvider {
|
||||
@NonNull
|
||||
String name;
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
for (BrowserEntry entry : getEntries()) {
|
||||
@@ -35,10 +35,10 @@ public class NewDirectoryActionProvider implements BrowserActionProvider {
|
||||
}
|
||||
model.refreshSync();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "newDirectory";
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
public class NewFileActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "newFile";
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
@@ -18,11 +23,6 @@ public class NewFileActionProvider implements BrowserActionProvider {
|
||||
@NonNull
|
||||
String name;
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
for (BrowserEntry entry : getEntries()) {
|
||||
@@ -35,10 +35,10 @@ public class NewFileActionProvider implements BrowserActionProvider {
|
||||
}
|
||||
model.refreshSync();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "newFile";
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,11 @@ import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
public class NewLinkActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "newLink";
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
@@ -22,11 +27,6 @@ public class NewLinkActionProvider implements BrowserActionProvider {
|
||||
@NonNull
|
||||
FilePath target;
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
for (BrowserEntry entry : getEntries()) {
|
||||
@@ -39,10 +39,10 @@ public class NewLinkActionProvider implements BrowserActionProvider {
|
||||
}
|
||||
model.refreshSync();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "newLink";
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-11
@@ -13,17 +13,6 @@ import java.util.List;
|
||||
|
||||
public class OpenDirectoryActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
|
||||
@Override
|
||||
public void executeImpl() {
|
||||
var first = getEntries().getFirst();
|
||||
model.cdSync(first.getRawFileEntry().getPath().toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "openDirectory";
|
||||
@@ -34,4 +23,15 @@ public class OpenDirectoryActionProvider implements BrowserActionProvider {
|
||||
return entries.size() == 1
|
||||
&& entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY);
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
|
||||
@Override
|
||||
public void executeImpl() {
|
||||
var first = getEntries().getFirst();
|
||||
model.cdSync(first.getRawFileEntry().getPath().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-11
@@ -14,6 +14,17 @@ import java.util.List;
|
||||
|
||||
public class OpenFileDefaultActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "openFileDefault";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return model.getFileList().getEditing().getValue() == null
|
||||
&& entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE);
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
@@ -25,15 +36,4 @@ public class OpenFileDefaultActionProvider implements BrowserActionProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "openFileDefault";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return model.getFileList().getEditing().getValue() == null
|
||||
&& entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE);
|
||||
}
|
||||
}
|
||||
|
||||
+22
-22
@@ -17,6 +17,17 @@ import java.util.List;
|
||||
|
||||
public class OpenFileNativeDetailsActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "openFileNativeDetails";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var sc = model.getFileSystem().getShell().orElseThrow();
|
||||
return sc.getLocalSystemAccess().supportsFileSystemAccess();
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
@@ -28,7 +39,7 @@ public class OpenFileNativeDetailsActionProvider implements BrowserActionProvide
|
||||
var e = entry.getRawFileEntry().getPath();
|
||||
var localFile = sc.getLocalSystemAccess().translateToLocalSystemPath(e);
|
||||
switch (OsType.getLocal()) {
|
||||
case OsType.Windows windows -> {
|
||||
case OsType.Windows ignored -> {
|
||||
var shell = LocalShell.getLocalPowershell();
|
||||
if (shell.isEmpty()) {
|
||||
return;
|
||||
@@ -50,11 +61,11 @@ public class OpenFileNativeDetailsActionProvider implements BrowserActionProvide
|
||||
// So let's keep one process running
|
||||
shell.get().command(content).notComplex().execute();
|
||||
}
|
||||
case OsType.Linux linux -> {
|
||||
case OsType.Linux ignored -> {
|
||||
var dbus = String.format(
|
||||
"""
|
||||
dbus-send --session --print-reply --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItemProperties array:string:"file://%s" string:""
|
||||
""",
|
||||
dbus-send --session --print-reply --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItemProperties array:string:"file://%s" string:""
|
||||
""",
|
||||
localFile);
|
||||
var success = sc.executeSimpleBooleanCommand(dbus);
|
||||
if (success) {
|
||||
@@ -69,15 +80,15 @@ public class OpenFileNativeDetailsActionProvider implements BrowserActionProvide
|
||||
: e.getParent()))
|
||||
.execute();
|
||||
}
|
||||
case OsType.MacOs macOs -> {
|
||||
case OsType.MacOs ignored -> {
|
||||
sc.osascriptCommand(String.format(
|
||||
"""
|
||||
set fileEntry to (POSIX file "%s") as text
|
||||
tell application "Finder"
|
||||
activate
|
||||
open information window of alias fileEntry
|
||||
end tell
|
||||
""",
|
||||
set fileEntry to (POSIX file "%s") as text
|
||||
tell application "Finder"
|
||||
activate
|
||||
open information window of alias fileEntry
|
||||
end tell
|
||||
""",
|
||||
localFile))
|
||||
.execute();
|
||||
}
|
||||
@@ -85,15 +96,4 @@ public class OpenFileNativeDetailsActionProvider implements BrowserActionProvide
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "openFileNativeDetails";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var sc = model.getFileSystem().getShell().orElseThrow();
|
||||
return sc.getLocalSystemAccess().supportsFileSystemAccess();
|
||||
}
|
||||
}
|
||||
|
||||
+12
-12
@@ -15,6 +15,18 @@ import java.util.List;
|
||||
|
||||
public class OpenFileWithActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "openFileWith";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return OsType.getLocal() == OsType.WINDOWS
|
||||
&& entries.size() == 1
|
||||
&& entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE);
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
@@ -26,16 +38,4 @@ public class OpenFileWithActionProvider implements BrowserActionProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "openFileWith";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return OsType.getLocal().equals(OsType.WINDOWS)
|
||||
&& entries.size() == 1
|
||||
&& entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE);
|
||||
}
|
||||
}
|
||||
|
||||
+16
-16
@@ -16,6 +16,22 @@ import java.util.List;
|
||||
|
||||
public class OpenTerminalActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "openTerminalInDirectory";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var t = AppPrefs.get().terminalType().getValue();
|
||||
return t != null;
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
@@ -37,20 +53,4 @@ public class OpenTerminalActionProvider implements BrowserActionProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "openTerminalInDirectory";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var t = AppPrefs.get().terminalType().getValue();
|
||||
return t != null;
|
||||
}
|
||||
}
|
||||
|
||||
+22
-18
@@ -2,15 +2,15 @@ package io.xpipe.app.browser.action.impl;
|
||||
|
||||
import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.browser.action.BrowserActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ProcessOutputException;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public class RunCommandInBackgroundActionProvider implements BrowserActionProvider {
|
||||
@@ -27,39 +27,43 @@ public class RunCommandInBackgroundActionProvider implements BrowserActionProvid
|
||||
@NonNull
|
||||
String command;
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
var cmd = CommandBuilder.of().add(command);
|
||||
for (BrowserEntry entry : getEntries()) {
|
||||
cmd.addFile(entry.getRawFileEntry().getPath());
|
||||
}
|
||||
|
||||
AtomicReference<String> out = new AtomicReference<>();
|
||||
AtomicReference<String> err = new AtomicReference<>();
|
||||
long exitCode;
|
||||
try (var command = model.getFileSystem()
|
||||
try (var cc = model.getFileSystem()
|
||||
.getShell()
|
||||
.orElseThrow()
|
||||
.command(cmd)
|
||||
.withWorkingDirectory(model.getCurrentDirectory().getPath())
|
||||
.command(command)
|
||||
.withWorkingDirectory(files.getFirst())
|
||||
.start()) {
|
||||
var r = command.readStdoutAndStderr();
|
||||
var r = cc.readStdoutAndStderr();
|
||||
out.set(r[0]);
|
||||
err.set(r[1]);
|
||||
exitCode = command.getExitCode();
|
||||
exitCode = cc.getExitCode();
|
||||
}
|
||||
|
||||
model.refreshBrowserEntriesSync(getEntries());
|
||||
model.refreshSync();
|
||||
|
||||
// Only throw actual error output
|
||||
if (exitCode != 0) {
|
||||
throw ErrorEventFactory.expected(ProcessOutputException.of(exitCode, out.get(), err.get()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> toDisplayMap() {
|
||||
var map = new LinkedHashMap<>(super.toDisplayMap());
|
||||
map.remove("Title");
|
||||
map.remove("Files");
|
||||
map.put("Working Directory", files.getFirst().toString());
|
||||
return map;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
-11
@@ -2,14 +2,15 @@ package io.xpipe.app.browser.action.impl;
|
||||
|
||||
import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.browser.action.BrowserActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.util.CommandDialog;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class RunCommandInBrowserActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
@@ -24,21 +25,28 @@ public class RunCommandInBrowserActionProvider implements BrowserActionProvider
|
||||
@NonNull
|
||||
String command;
|
||||
|
||||
@Override
|
||||
public void executeImpl() {
|
||||
var cmd = model.getFileSystem()
|
||||
.getShell()
|
||||
.orElseThrow()
|
||||
.command(command)
|
||||
.withWorkingDirectory(files.getFirst());
|
||||
CommandDialog.runAndShow(cmd);
|
||||
model.refreshSync();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeImpl() {
|
||||
var builder = CommandBuilder.of().add(command);
|
||||
for (BrowserEntry entry : getEntries()) {
|
||||
builder.addFile(entry.getRawFileEntry().getPath());
|
||||
}
|
||||
|
||||
var cmd = model.getFileSystem().getShell().orElseThrow().command(builder);
|
||||
CommandDialog.runAndShow(cmd);
|
||||
model.refreshBrowserEntriesSync(getEntries());
|
||||
public Map<String, String> toDisplayMap() {
|
||||
var map = new LinkedHashMap<>(super.toDisplayMap());
|
||||
map.remove("Files");
|
||||
map.put("Working Directory", files.getFirst().toString());
|
||||
return map;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-16
@@ -2,13 +2,14 @@ package io.xpipe.app.browser.action.impl;
|
||||
|
||||
import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.browser.action.BrowserActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class RunCommandInTerminalActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
@@ -20,31 +21,37 @@ public class RunCommandInTerminalActionProvider implements BrowserActionProvider
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
|
||||
@NonNull
|
||||
String title;
|
||||
|
||||
@NonNull
|
||||
String command;
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
var wd = files.getFirst();
|
||||
model.openTerminalSync(
|
||||
title,
|
||||
wd,
|
||||
model.getFileSystem()
|
||||
.getShell()
|
||||
.orElseThrow()
|
||||
.command(command)
|
||||
.withWorkingDirectory(wd),
|
||||
true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
var cmd = CommandBuilder.of().add(command);
|
||||
for (BrowserEntry entry : getEntries()) {
|
||||
cmd.addFile(entry.getRawFileEntry().getPath());
|
||||
}
|
||||
|
||||
model.openTerminalSync(
|
||||
title,
|
||||
model.getCurrentDirectory() != null
|
||||
? model.getCurrentDirectory().getPath()
|
||||
: null,
|
||||
model.getFileSystem().getShell().orElseThrow().command(cmd),
|
||||
true);
|
||||
public Map<String, String> toDisplayMap() {
|
||||
var map = new LinkedHashMap<>(super.toDisplayMap());
|
||||
map.remove("Title");
|
||||
map.remove("Files");
|
||||
map.put("Working Directory", files.getFirst().toString());
|
||||
return map;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,11 @@ public class TransferFilesActionProvider implements ActionProvider {
|
||||
|
||||
boolean download;
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
operation.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return !download;
|
||||
@@ -47,11 +52,6 @@ public class TransferFilesActionProvider implements ActionProvider {
|
||||
return operation.isMove();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
operation.execute();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> toDisplayMap() {
|
||||
var name = operation.isMove() ? "Move files" : getDisplayName();
|
||||
|
||||
@@ -4,9 +4,9 @@ import io.xpipe.app.ext.FileEntry;
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.issue.ErrorEventFactory;
|
||||
import io.xpipe.app.util.GlobalClipboard;
|
||||
import io.xpipe.app.util.GlobalObjectProperty;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.DataFormat;
|
||||
import javafx.scene.input.Dragboard;
|
||||
@@ -26,9 +26,9 @@ import java.util.stream.Collectors;
|
||||
|
||||
public class BrowserClipboard {
|
||||
|
||||
public static final Property<Instance> currentCopyClipboard = new SimpleObjectProperty<>();
|
||||
public static Instance currentDragClipboard;
|
||||
public static final Property<Instance> currentCopyClipboard = new GlobalObjectProperty<>();
|
||||
private static final DataFormat DATA_FORMAT = new DataFormat("application/xpipe-file-list");
|
||||
public static Instance currentDragClipboard;
|
||||
|
||||
static {
|
||||
GlobalClipboard.addListener(new Consumer<>() {
|
||||
|
||||
+2
-2
@@ -8,12 +8,12 @@ import io.xpipe.core.FilePath;
|
||||
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
|
||||
public class BrowserAlerts {
|
||||
public class BrowserDialogs {
|
||||
|
||||
public static FileConflictChoice showFileConflictAlert(FilePath file, boolean multiple) {
|
||||
var choice = new SimpleObjectProperty<FileConflictChoice>();
|
||||
var key = multiple ? "fileConflictAlertContentMultiple" : "fileConflictAlertContent";
|
||||
var w = multiple ? 700 : 400;
|
||||
var w = multiple ? 1050 : 400;
|
||||
var modal = ModalOverlay.of(
|
||||
"fileConflictAlertTitle",
|
||||
AppDialog.dialogText(AppI18n.observable(key, file)).prefWidth(w));
|
||||
@@ -2,6 +2,7 @@ package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.browser.menu.BrowserMenuProviders;
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.FileEntry;
|
||||
import io.xpipe.app.util.*;
|
||||
@@ -36,7 +37,6 @@ import static javafx.scene.control.TableColumn.SortType.ASCENDING;
|
||||
|
||||
public final class BrowserFileListComp extends SimpleComp {
|
||||
|
||||
private static final PseudoClass HIDDEN = PseudoClass.getPseudoClass("hidden");
|
||||
private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
|
||||
private static final PseudoClass FILE = PseudoClass.getPseudoClass("file");
|
||||
private static final PseudoClass FOLDER = PseudoClass.getPseudoClass("folder");
|
||||
@@ -51,6 +51,18 @@ public final class BrowserFileListComp extends SimpleComp {
|
||||
this.fileList = fileList;
|
||||
}
|
||||
|
||||
private static void prepareTableScrollFix(TableView<BrowserEntry> table) {
|
||||
table.lookupAll(".scroll-bar").stream()
|
||||
.filter(node -> node.getPseudoClassStates().contains(PseudoClass.getPseudoClass("horizontal")))
|
||||
.findFirst()
|
||||
.ifPresent(node -> {
|
||||
Region region = (Region) node;
|
||||
region.setMinHeight(0);
|
||||
region.setPrefHeight(0);
|
||||
region.setMaxHeight(0);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Region createSimple() {
|
||||
return createTable();
|
||||
@@ -76,9 +88,15 @@ public final class BrowserFileListComp extends SimpleComp {
|
||||
sizeCol.setCellValueFactory(param -> new ReadOnlyStringWrapper(
|
||||
param.getValue().getRawFileEntry().resolved().getSize()));
|
||||
sizeCol.setComparator((size1, size2) -> {
|
||||
if (size1 == null && size2 == null) return 0;
|
||||
if (size1 == null) return -1;
|
||||
if (size2 == null) return 1;
|
||||
if (size1 == null && size2 == null) {
|
||||
return 0;
|
||||
}
|
||||
if (size1 == null) {
|
||||
return -1;
|
||||
}
|
||||
if (size2 == null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
long long1 = Long.parseLong(size1);
|
||||
@@ -126,7 +144,16 @@ public final class BrowserFileListComp extends SimpleComp {
|
||||
var table = new TableView<BrowserEntry>();
|
||||
table.setSkin(new TableViewSkin<>(table));
|
||||
table.setAccessibleText("Directory contents");
|
||||
table.setPlaceholder(new Region());
|
||||
|
||||
var placeholder = new Label();
|
||||
fileList.getFileSystemModel().getBusy().subscribe(busy -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
placeholder.setText(busy ? null : AppI18n.get("emptyDirectory"));
|
||||
});
|
||||
});
|
||||
table.setPlaceholder(placeholder);
|
||||
AppFontSizes.base(placeholder);
|
||||
|
||||
table.getStyleClass().add(Styles.STRIPED);
|
||||
table.getColumns().setAll(filenameCol, mtimeCol, modeCol, ownerCol, sizeCol);
|
||||
table.getSortOrder().add(filenameCol);
|
||||
@@ -148,18 +175,6 @@ public final class BrowserFileListComp extends SimpleComp {
|
||||
return table;
|
||||
}
|
||||
|
||||
private static void prepareTableScrollFix(TableView<BrowserEntry> table) {
|
||||
table.lookupAll(".scroll-bar").stream()
|
||||
.filter(node -> node.getPseudoClassStates().contains(PseudoClass.getPseudoClass("horizontal")))
|
||||
.findFirst()
|
||||
.ifPresent(node -> {
|
||||
Region region = (Region) node;
|
||||
region.setMinHeight(0);
|
||||
region.setPrefHeight(0);
|
||||
region.setMaxHeight(0);
|
||||
});
|
||||
}
|
||||
|
||||
private void prepareColumnVisibility(
|
||||
TableView<BrowserEntry> table,
|
||||
TableColumn<BrowserEntry, String> filenameCol,
|
||||
|
||||
@@ -190,23 +190,23 @@ public class BrowserFileOpener {
|
||||
|
||||
return new BrowserFileOutput() {
|
||||
@Override
|
||||
public void beforeTransfer() {}
|
||||
public Optional<DataStoreEntry> target() {
|
||||
return Optional.of(model.getEntry().get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasOutput() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<DataStoreEntry> target() {
|
||||
return Optional.of(model.getEntry().get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream open() throws Exception {
|
||||
return entry.getFileSystem().openOutput(file, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTransfer() {}
|
||||
|
||||
@Override
|
||||
public void onFinish() {
|
||||
model.refreshFileEntriesSync(List.of(entry));
|
||||
@@ -239,23 +239,23 @@ public class BrowserFileOpener {
|
||||
|
||||
return new BrowserFileOutput() {
|
||||
@Override
|
||||
public void beforeTransfer() {}
|
||||
public Optional<DataStoreEntry> target() {
|
||||
return Optional.of(model.getEntry().get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasOutput() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<DataStoreEntry> target() {
|
||||
return Optional.of(model.getEntry().get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream open() throws Exception {
|
||||
return entry.getFileSystem().openOutput(file, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTransfer() {}
|
||||
|
||||
@Override
|
||||
public void onFinish() {
|
||||
model.refreshFileEntriesSync(List.of(entry));
|
||||
|
||||
@@ -35,7 +35,7 @@ public class BrowserFileSystemHelper {
|
||||
return path;
|
||||
}
|
||||
|
||||
if (shell.get().getOsType().equals(OsType.WINDOWS) && path.length() == 2 && path.endsWith(":")) {
|
||||
if (shell.get().getOsType() == OsType.WINDOWS && path.length() == 2 && path.endsWith(":")) {
|
||||
return path + "\\";
|
||||
}
|
||||
|
||||
|
||||
@@ -58,10 +58,6 @@ public final class BrowserFileSystemHistory {
|
||||
cursor.set(history.size() - 1);
|
||||
}
|
||||
|
||||
public FilePath back() {
|
||||
return back(1);
|
||||
}
|
||||
|
||||
public FilePath back(int i) {
|
||||
if (!canGoBack.get()) {
|
||||
return null;
|
||||
|
||||
@@ -20,13 +20,13 @@ import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.terminal.*;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.FailableConsumer;
|
||||
import io.xpipe.core.FileKind;
|
||||
import io.xpipe.core.FilePath;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
@@ -35,6 +35,8 @@ import lombok.NonNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
@@ -50,12 +52,14 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
private final ReadOnlyObjectWrapper<FilePath> currentPath = new ReadOnlyObjectWrapper<>();
|
||||
private final BrowserFileSystemHistory history = new BrowserFileSystemHistory();
|
||||
private final BooleanProperty inOverview = new SimpleBooleanProperty();
|
||||
private final Property<BrowserTransferProgress> progress = new SimpleObjectProperty<>();
|
||||
private final ObservableList<UUID> terminalRequests = FXCollections.observableArrayList();
|
||||
private final BooleanProperty transferCancelled = new SimpleBooleanProperty();
|
||||
|
||||
private final Property<BrowserTransferProgress> progress = new SimpleObjectProperty<>();
|
||||
private final ObservableList<BrowserTransferProgress> progressesIntervalHistory =
|
||||
FXCollections.observableArrayList();
|
||||
private final LongProperty progressTransferSpeed = new SimpleLongProperty();
|
||||
private final Property<Duration> progressRemaining = new SimpleObjectProperty<>();
|
||||
private FileSystem fileSystem;
|
||||
|
||||
private BrowserFileSystemSavedState savedState;
|
||||
private BrowserFileSystemCache cache;
|
||||
|
||||
@@ -72,6 +76,50 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
fileList = new BrowserFileListModel(selectionMode, this);
|
||||
}
|
||||
|
||||
public void updateProgress(BrowserTransferProgress n) {
|
||||
if (n == null) {
|
||||
progress.setValue(null);
|
||||
progressesIntervalHistory.clear();
|
||||
progressTransferSpeed.setValue(0);
|
||||
return;
|
||||
}
|
||||
|
||||
var changedHistory = false;
|
||||
if (progress.getValue() != null) {
|
||||
var last = progressesIntervalHistory.isEmpty()
|
||||
? Instant.EPOCH
|
||||
: progressesIntervalHistory.getLast().getTimestamp();
|
||||
var elapsed = Duration.between(last, n.getTimestamp());
|
||||
if (elapsed.toMillis() >= 1000) {
|
||||
progressesIntervalHistory.add(progress.getValue());
|
||||
changedHistory = true;
|
||||
}
|
||||
}
|
||||
|
||||
progress.setValue(n);
|
||||
if (progressesIntervalHistory.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (changedHistory && progressesIntervalHistory.size() >= 2) {
|
||||
var speed = BrowserTransferProgress.estimateTransferSpeed(progressesIntervalHistory, n);
|
||||
progressTransferSpeed.setValue(speed);
|
||||
var remaining = n.getTotal() - n.getTransferred();
|
||||
var estimate = remaining / (double) speed;
|
||||
|
||||
var newDuration = Duration.ofMillis((long) (estimate * 1000.0));
|
||||
var smooth = progressRemaining.getValue() != null
|
||||
&& progressRemaining.getValue().toSeconds() + 1 == newDuration.toSeconds();
|
||||
if (!smooth) {
|
||||
progressRemaining.setValue(newDuration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableValue<BrowserTransferProgress> getProgress() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
public Optional<FileEntry> findFile(FilePath path) {
|
||||
return getFileList().getAll().getValue().stream()
|
||||
.filter(browserEntry -> browserEntry.getFileName().equals(path.toString())
|
||||
@@ -151,6 +199,9 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
var s = fileSystem.getShell();
|
||||
if (s.isPresent()) {
|
||||
s.get().start();
|
||||
if (s.get().isAnyStreamClosed()) {
|
||||
s.get().restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,11 +337,15 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
|
||||
// Evaluate optional expressions
|
||||
String evaluatedPath;
|
||||
try {
|
||||
evaluatedPath = BrowserFileSystemHelper.evaluatePath(this, adjustedPath);
|
||||
} catch (Exception ex) {
|
||||
ErrorEventFactory.fromThrowable(ex).handle();
|
||||
return Optional.ofNullable(cps);
|
||||
if (customInput) {
|
||||
try {
|
||||
evaluatedPath = BrowserFileSystemHelper.evaluatePath(this, adjustedPath);
|
||||
} catch (Exception ex) {
|
||||
ErrorEventFactory.fromThrowable(ex).handle();
|
||||
return Optional.ofNullable(cps);
|
||||
}
|
||||
} else {
|
||||
evaluatedPath = adjustedPath;
|
||||
}
|
||||
|
||||
if (evaluatedPath == null) {
|
||||
@@ -368,20 +423,6 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
loadFilesSync(path);
|
||||
}
|
||||
|
||||
public void withFiles(FilePath dir, FailableConsumer<Stream<FileEntry>, Exception> consumer) throws Exception {
|
||||
BooleanScope.executeExclusive(busy, () -> {
|
||||
if (dir != null) {
|
||||
startIfNeeded();
|
||||
var fs = getFileSystem();
|
||||
|
||||
var stream = fs.listFiles(fs, dir);
|
||||
consumer.accept(stream);
|
||||
} else {
|
||||
consumer.accept(Stream.of());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean loadFilesSync(FilePath dir) {
|
||||
try {
|
||||
startIfNeeded();
|
||||
@@ -405,7 +446,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
BooleanScope.executeExclusive(busy, () -> {
|
||||
startIfNeeded();
|
||||
var op = BrowserFileTransferOperation.ofLocal(
|
||||
entry, files, BrowserFileTransferMode.COPY, true, progress::setValue, transferCancelled);
|
||||
entry, files, BrowserFileTransferMode.COPY, true, p -> updateProgress(p), transferCancelled);
|
||||
var action = TransferFilesActionProvider.Action.builder()
|
||||
.operation(op)
|
||||
.target(this.entry.asNeeded())
|
||||
@@ -427,7 +468,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
BooleanScope.executeExclusive(busy, () -> {
|
||||
startIfNeeded();
|
||||
var op = new BrowserFileTransferOperation(
|
||||
target, files, mode, true, progress::setValue, transferCancelled);
|
||||
target, files, mode, true, this::updateProgress, transferCancelled);
|
||||
var action = TransferFilesActionProvider.Action.builder()
|
||||
.operation(op)
|
||||
.target(entry.asNeeded())
|
||||
@@ -470,7 +511,14 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
&& !(fullSessionModel.getSplits().get(this) instanceof BrowserTerminalDockTabModel)) {
|
||||
fullSessionModel.splitTab(this, new BrowserTerminalDockTabModel(browserModel, this, terminalRequests));
|
||||
}
|
||||
TerminalLauncher.open(entry.get(), name, directory, processControl, uuid, !dock);
|
||||
TerminalLaunch.builder()
|
||||
.entry(entry.get())
|
||||
.title(name)
|
||||
.directory(directory)
|
||||
.command(processControl)
|
||||
.request(uuid)
|
||||
.preferTabs(!dock)
|
||||
.launch();
|
||||
|
||||
// Restart connection as we will have to start it anyway, so we speed it up by doing it preemptively
|
||||
startIfNeeded();
|
||||
|
||||
@@ -17,7 +17,6 @@ import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -28,6 +27,8 @@ import java.util.regex.Pattern;
|
||||
|
||||
public class BrowserFileTransferOperation {
|
||||
|
||||
private static final int DEFAULT_BUFFER_SIZE = 1024;
|
||||
|
||||
@Getter
|
||||
private final FileEntry target;
|
||||
|
||||
@@ -38,8 +39,7 @@ public class BrowserFileTransferOperation {
|
||||
private final boolean checkConflicts;
|
||||
private final Consumer<BrowserTransferProgress> progress;
|
||||
private final BooleanProperty cancelled;
|
||||
|
||||
BrowserAlerts.FileConflictChoice lastConflictChoice;
|
||||
BrowserDialogs.FileConflictChoice lastConflictChoice;
|
||||
|
||||
public BrowserFileTransferOperation(
|
||||
FileEntry target,
|
||||
@@ -84,53 +84,53 @@ public class BrowserFileTransferOperation {
|
||||
this.progress.accept(progress);
|
||||
}
|
||||
|
||||
private BrowserAlerts.FileConflictChoice handleChoice(FileSystem fileSystem, FilePath target, boolean multiple)
|
||||
private BrowserDialogs.FileConflictChoice handleChoice(FileSystem fileSystem, FilePath target, boolean multiple)
|
||||
throws Exception {
|
||||
if (lastConflictChoice == BrowserAlerts.FileConflictChoice.CANCEL) {
|
||||
return BrowserAlerts.FileConflictChoice.CANCEL;
|
||||
if (lastConflictChoice == BrowserDialogs.FileConflictChoice.CANCEL) {
|
||||
return BrowserDialogs.FileConflictChoice.CANCEL;
|
||||
}
|
||||
|
||||
if (lastConflictChoice == BrowserAlerts.FileConflictChoice.REPLACE_ALL) {
|
||||
return BrowserAlerts.FileConflictChoice.REPLACE;
|
||||
if (lastConflictChoice == BrowserDialogs.FileConflictChoice.REPLACE_ALL) {
|
||||
return BrowserDialogs.FileConflictChoice.REPLACE;
|
||||
}
|
||||
|
||||
if (lastConflictChoice == BrowserAlerts.FileConflictChoice.RENAME_ALL) {
|
||||
return BrowserAlerts.FileConflictChoice.RENAME;
|
||||
if (lastConflictChoice == BrowserDialogs.FileConflictChoice.RENAME_ALL) {
|
||||
return BrowserDialogs.FileConflictChoice.RENAME;
|
||||
}
|
||||
|
||||
if (fileSystem.fileExists(target)) {
|
||||
if (lastConflictChoice == BrowserAlerts.FileConflictChoice.SKIP_ALL) {
|
||||
return BrowserAlerts.FileConflictChoice.SKIP;
|
||||
if (lastConflictChoice == BrowserDialogs.FileConflictChoice.SKIP_ALL) {
|
||||
return BrowserDialogs.FileConflictChoice.SKIP;
|
||||
}
|
||||
|
||||
var choice = BrowserAlerts.showFileConflictAlert(target, multiple);
|
||||
if (choice == BrowserAlerts.FileConflictChoice.CANCEL) {
|
||||
lastConflictChoice = BrowserAlerts.FileConflictChoice.CANCEL;
|
||||
return BrowserAlerts.FileConflictChoice.CANCEL;
|
||||
var choice = BrowserDialogs.showFileConflictAlert(target, multiple);
|
||||
if (choice == BrowserDialogs.FileConflictChoice.CANCEL) {
|
||||
lastConflictChoice = BrowserDialogs.FileConflictChoice.CANCEL;
|
||||
return BrowserDialogs.FileConflictChoice.CANCEL;
|
||||
}
|
||||
|
||||
if (choice == BrowserAlerts.FileConflictChoice.SKIP) {
|
||||
return BrowserAlerts.FileConflictChoice.SKIP;
|
||||
if (choice == BrowserDialogs.FileConflictChoice.SKIP) {
|
||||
return BrowserDialogs.FileConflictChoice.SKIP;
|
||||
}
|
||||
|
||||
if (choice == BrowserAlerts.FileConflictChoice.SKIP_ALL) {
|
||||
lastConflictChoice = BrowserAlerts.FileConflictChoice.SKIP_ALL;
|
||||
return BrowserAlerts.FileConflictChoice.SKIP;
|
||||
if (choice == BrowserDialogs.FileConflictChoice.SKIP_ALL) {
|
||||
lastConflictChoice = BrowserDialogs.FileConflictChoice.SKIP_ALL;
|
||||
return BrowserDialogs.FileConflictChoice.SKIP;
|
||||
}
|
||||
|
||||
if (choice == BrowserAlerts.FileConflictChoice.REPLACE_ALL) {
|
||||
lastConflictChoice = BrowserAlerts.FileConflictChoice.REPLACE_ALL;
|
||||
return BrowserAlerts.FileConflictChoice.REPLACE;
|
||||
if (choice == BrowserDialogs.FileConflictChoice.REPLACE_ALL) {
|
||||
lastConflictChoice = BrowserDialogs.FileConflictChoice.REPLACE_ALL;
|
||||
return BrowserDialogs.FileConflictChoice.REPLACE;
|
||||
}
|
||||
|
||||
if (choice == BrowserAlerts.FileConflictChoice.RENAME_ALL) {
|
||||
lastConflictChoice = BrowserAlerts.FileConflictChoice.RENAME_ALL;
|
||||
return BrowserAlerts.FileConflictChoice.RENAME;
|
||||
if (choice == BrowserDialogs.FileConflictChoice.RENAME_ALL) {
|
||||
lastConflictChoice = BrowserDialogs.FileConflictChoice.RENAME_ALL;
|
||||
return BrowserDialogs.FileConflictChoice.RENAME;
|
||||
}
|
||||
|
||||
return choice;
|
||||
}
|
||||
return BrowserAlerts.FileConflictChoice.REPLACE;
|
||||
return BrowserDialogs.FileConflictChoice.REPLACE;
|
||||
}
|
||||
|
||||
private boolean cancelled() {
|
||||
@@ -172,7 +172,11 @@ public class BrowserFileTransferOperation {
|
||||
var currentDir =
|
||||
file.getFileSystem().getShell().orElseThrow().view().pwd();
|
||||
handleSingleAcrossFileSystems(file);
|
||||
file.getFileSystem().getShell().orElseThrow().view().cd(currentDir);
|
||||
|
||||
// Expect a kill
|
||||
if (!file.getFileSystem().getShell().orElseThrow().isAnyStreamClosed()) {
|
||||
file.getFileSystem().getShell().orElseThrow().view().cd(currentDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,12 +217,12 @@ public class BrowserFileTransferOperation {
|
||||
|
||||
if (checkConflicts) {
|
||||
var fileConflictChoice = handleChoice(target.getFileSystem(), targetFile, files.size() > 1);
|
||||
if (fileConflictChoice == BrowserAlerts.FileConflictChoice.SKIP
|
||||
|| fileConflictChoice == BrowserAlerts.FileConflictChoice.CANCEL) {
|
||||
if (fileConflictChoice == BrowserDialogs.FileConflictChoice.SKIP
|
||||
|| fileConflictChoice == BrowserDialogs.FileConflictChoice.CANCEL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileConflictChoice == BrowserAlerts.FileConflictChoice.RENAME) {
|
||||
if (fileConflictChoice == BrowserDialogs.FileConflictChoice.RENAME) {
|
||||
targetFile = renameFileLoop(target.getFileSystem(), targetFile, source.getKind() == FileKind.DIRECTORY);
|
||||
}
|
||||
}
|
||||
@@ -284,19 +288,24 @@ public class BrowserFileTransferOperation {
|
||||
flatFiles.put(source, directoryName);
|
||||
|
||||
var baseRelative = source.getPath().getParent().toDirectory();
|
||||
List<FileEntry> list =
|
||||
source.getFileSystem().listFilesRecursively(source.getFileSystem(), source.getPath());
|
||||
for (FileEntry fileEntry : list) {
|
||||
if (cancelled()) {
|
||||
return;
|
||||
}
|
||||
try (var stream = source.getFileSystem().listFilesRecursively(source.getFileSystem(), source.getPath())) {
|
||||
List<FileEntry> list = stream.toList();
|
||||
for (FileEntry fileEntry : list) {
|
||||
if (cancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var rel = fileEntry.getPath().relativize(baseRelative).toUnix().toString();
|
||||
flatFiles.put(fileEntry, rel);
|
||||
if (fileEntry.getKind() == FileKind.FILE) {
|
||||
// This one is up-to-date and does not need to be recalculated
|
||||
// If we don't have a size, it doesn't matter that much as the total size is only for display
|
||||
totalSize.addAndGet(fileEntry.getFileSizeLong().orElse(0));
|
||||
var rel = fileEntry
|
||||
.getPath()
|
||||
.relativize(baseRelative)
|
||||
.toUnix()
|
||||
.toString();
|
||||
flatFiles.put(fileEntry, rel);
|
||||
if (fileEntry.getKind() == FileKind.FILE) {
|
||||
// This one is up-to-date and does not need to be recalculated
|
||||
// If we don't have a size, it doesn't matter that much as the total size is only for display
|
||||
totalSize.addAndGet(fileEntry.getFileSizeLong().orElse(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -321,7 +330,6 @@ public class BrowserFileTransferOperation {
|
||||
var targetFs = target.getFileSystem().createTransferOptimizedFileSystem();
|
||||
|
||||
try {
|
||||
var start = Instant.now();
|
||||
AtomicLong transferred = new AtomicLong();
|
||||
for (var e : flatFiles.entrySet()) {
|
||||
if (cancelled()) {
|
||||
@@ -342,24 +350,17 @@ public class BrowserFileTransferOperation {
|
||||
if (checkConflicts) {
|
||||
var fileConflictChoice =
|
||||
handleChoice(targetFs, targetFile, files.size() > 1 || flatFiles.size() > 1);
|
||||
if (fileConflictChoice == BrowserAlerts.FileConflictChoice.SKIP
|
||||
|| fileConflictChoice == BrowserAlerts.FileConflictChoice.CANCEL) {
|
||||
if (fileConflictChoice == BrowserDialogs.FileConflictChoice.SKIP
|
||||
|| fileConflictChoice == BrowserDialogs.FileConflictChoice.CANCEL) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fileConflictChoice == BrowserAlerts.FileConflictChoice.RENAME) {
|
||||
if (fileConflictChoice == BrowserDialogs.FileConflictChoice.RENAME) {
|
||||
targetFile = renameFileLoop(targetFs, targetFile, false);
|
||||
}
|
||||
}
|
||||
|
||||
transfer(
|
||||
sourceFile.getPath(),
|
||||
optimizedSourceFs,
|
||||
targetFile,
|
||||
targetFs,
|
||||
transferred,
|
||||
totalSize,
|
||||
start);
|
||||
transfer(sourceFile.getPath(), optimizedSourceFs, targetFile, targetFs, transferred, totalSize);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -380,8 +381,7 @@ public class BrowserFileTransferOperation {
|
||||
FilePath targetFile,
|
||||
FileSystem targetFs,
|
||||
AtomicLong transferred,
|
||||
AtomicLong totalSize,
|
||||
Instant start)
|
||||
AtomicLong totalSize)
|
||||
throws Exception {
|
||||
if (cancelled()) {
|
||||
return;
|
||||
@@ -407,7 +407,7 @@ public class BrowserFileTransferOperation {
|
||||
}
|
||||
|
||||
outputStream = targetFs.openOutput(targetFile, fileSize);
|
||||
transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, start, fileSize);
|
||||
transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, fileSize);
|
||||
} catch (Exception ex) {
|
||||
// Mark progress as finished to reset any progress display
|
||||
updateProgress(BrowserTransferProgress.finished(sourceFile.getFileName(), transferred.get()));
|
||||
@@ -475,19 +475,16 @@ public class BrowserFileTransferOperation {
|
||||
source.getFileSystem().delete(source.getPath());
|
||||
}
|
||||
|
||||
private static final int DEFAULT_BUFFER_SIZE = 1024;
|
||||
|
||||
private void transferFile(
|
||||
FilePath sourceFile,
|
||||
InputStream inputStream,
|
||||
OutputStream outputStream,
|
||||
AtomicLong transferred,
|
||||
AtomicLong total,
|
||||
Instant start,
|
||||
long expectedFileSize)
|
||||
throws Exception {
|
||||
// Initialize progress immediately prior to reading anything
|
||||
updateProgress(new BrowserTransferProgress(sourceFile.getFileName(), transferred.get(), total.get(), start));
|
||||
updateProgress(new BrowserTransferProgress(sourceFile.getFileName(), transferred.get(), total.get()));
|
||||
|
||||
var killStreams = new AtomicBoolean(false);
|
||||
var exception = new AtomicReference<Exception>();
|
||||
@@ -511,8 +508,8 @@ public class BrowserFileTransferOperation {
|
||||
outputStream.write(buffer, 0, read);
|
||||
transferred.addAndGet(read);
|
||||
readCount.addAndGet(read);
|
||||
updateProgress(new BrowserTransferProgress(
|
||||
sourceFile.getFileName(), transferred.get(), total.get(), start));
|
||||
updateProgress(
|
||||
new BrowserTransferProgress(sourceFile.getFileName(), transferred.get(), total.get()));
|
||||
}
|
||||
|
||||
outputStream.flush();
|
||||
|
||||
@@ -5,7 +5,6 @@ import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.core.OsType;
|
||||
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Region;
|
||||
@@ -25,9 +24,7 @@ public class BrowserGreetingComp extends SimpleComp {
|
||||
});
|
||||
});
|
||||
AppFontSizes.title(r);
|
||||
if (OsType.getLocal() != OsType.MACOS) {
|
||||
r.getStyleClass().add(Styles.TEXT_BOLD);
|
||||
}
|
||||
r.getStyleClass().add(Styles.TEXT_BOLD);
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ import java.util.List;
|
||||
@JsonDeserialize(using = BrowserHistorySavedStateImpl.Deserializer.class)
|
||||
public class BrowserHistorySavedStateImpl implements BrowserHistorySavedState {
|
||||
|
||||
private static BrowserHistorySavedStateImpl INSTANCE;
|
||||
|
||||
@JsonSerialize(as = List.class)
|
||||
ObservableList<Entry> lastSystems;
|
||||
|
||||
@@ -29,8 +31,6 @@ public class BrowserHistorySavedStateImpl implements BrowserHistorySavedState {
|
||||
this.lastSystems = FXCollections.synchronizedObservableList(FXCollections.observableArrayList(lastSystems));
|
||||
}
|
||||
|
||||
private static BrowserHistorySavedStateImpl INSTANCE;
|
||||
|
||||
public static BrowserHistorySavedState get() {
|
||||
if (INSTANCE == null) {
|
||||
INSTANCE = load();
|
||||
|
||||
@@ -17,6 +17,8 @@ public class BrowserLocalFileSystem {
|
||||
if (localFileSystem == null) {
|
||||
localFileSystem = new LocalStore().createFileSystem();
|
||||
localFileSystem.open();
|
||||
} else if (localFileSystem.getShell().orElseThrow().isAnyStreamClosed()) {
|
||||
localFileSystem.getShell().orElseThrow().restart();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,10 +30,7 @@ public class BrowserLocalFileSystem {
|
||||
}
|
||||
|
||||
public static FileEntry getLocalFileEntry(Path file) throws Exception {
|
||||
if (localFileSystem == null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
init();
|
||||
return new FileEntry(
|
||||
localFileSystem.open(),
|
||||
FilePath.of(file),
|
||||
|
||||
@@ -34,6 +34,13 @@ import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
|
||||
|
||||
private static final PseudoClass INVISIBLE = PseudoClass.getPseudoClass("invisible");
|
||||
private final BrowserFileSystemTabModel model;
|
||||
|
||||
public BrowserNavBarComp(BrowserFileSystemTabModel model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Structure createBase() {
|
||||
var pathBar = createPathBar();
|
||||
@@ -185,22 +192,6 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
|
||||
return pathBar;
|
||||
}
|
||||
|
||||
public record Structure(HBox box, TextField textField, Button historyButton) implements CompStructure<HBox> {
|
||||
|
||||
@Override
|
||||
public HBox get() {
|
||||
return box;
|
||||
}
|
||||
}
|
||||
|
||||
private static final PseudoClass INVISIBLE = PseudoClass.getPseudoClass("invisible");
|
||||
|
||||
private final BrowserFileSystemTabModel model;
|
||||
|
||||
public BrowserNavBarComp(BrowserFileSystemTabModel model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
private ContextMenu createContextMenu() {
|
||||
var cm = ContextMenuHelper.create();
|
||||
|
||||
@@ -263,4 +254,12 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
|
||||
});
|
||||
return cm;
|
||||
}
|
||||
|
||||
public record Structure(HBox box, TextField textField, Button historyButton) implements CompStructure<HBox> {
|
||||
|
||||
@Override
|
||||
public HBox get() {
|
||||
return box;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import io.xpipe.app.comp.base.PrettyImageHelper;
|
||||
import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.ext.FileEntry;
|
||||
import io.xpipe.app.util.BooleanAnimationTimer;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.InputHelper;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.FileKind;
|
||||
@@ -94,9 +95,10 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
|
||||
|
||||
private List<MenuItem> updateMenuItems(Menu m, BrowserEntry entry, boolean updateInstantly) throws Exception {
|
||||
List<FileEntry> list = new ArrayList<>();
|
||||
model.withFiles(entry.getRawFileEntry().resolved().getPath(), newFiles -> {
|
||||
try (var s = newFiles) {
|
||||
var l = s.map(fileEntry -> fileEntry.resolved()).toList();
|
||||
BooleanScope.executeExclusive(model.getBusy(), () -> {
|
||||
var dir = entry.getRawFileEntry().resolved().getPath();
|
||||
try (var stream = model.getFileSystem().listFiles(model.getFileSystem(), dir)) {
|
||||
var l = stream.map(fileEntry -> fileEntry.resolved()).toList();
|
||||
// Wait until all files are listed, i.e. do not skip the stream elements
|
||||
list.addAll(l.subList(0, Math.min(l.size(), 150)));
|
||||
}
|
||||
@@ -138,8 +140,8 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
|
||||
class QuickAccessMenu {
|
||||
private final BrowserEntry browserEntry;
|
||||
private final Menu menu;
|
||||
private ContextMenu browserActionMenu;
|
||||
private final MenuItem empty;
|
||||
private ContextMenu browserActionMenu;
|
||||
|
||||
public QuickAccessMenu(BrowserEntry browserEntry) {
|
||||
empty = new Menu("...");
|
||||
|
||||
@@ -21,8 +21,6 @@ import javafx.scene.layout.Region;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
|
||||
@Value
|
||||
@@ -81,21 +79,33 @@ public class BrowserStatusBarComp extends SimpleComp {
|
||||
}
|
||||
|
||||
private Comp<?> createProgressEstimateStatus() {
|
||||
var text = BindingsHelper.map(model.getProgress(), p -> {
|
||||
if (p == null) {
|
||||
return null;
|
||||
} else {
|
||||
var expected = p.expectedTimeRemaining();
|
||||
var show = p.elapsedTime().compareTo(Duration.of(200, ChronoUnit.MILLIS)) > 0
|
||||
&& (!p.hasKnownTotalSize() || p.getTotal() > 50_000_000 || expected.toMillis() > 5000);
|
||||
var time = show ? HumanReadableFormat.duration(p.expectedTimeRemaining()) : "";
|
||||
return time;
|
||||
}
|
||||
});
|
||||
var text = Bindings.createStringBinding(
|
||||
() -> {
|
||||
var p = model.getProgress().getValue();
|
||||
var expected = model.getProgressRemaining().getValue();
|
||||
if (p == null || expected == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var elapsed = (p.getTotal() - p.getTransferred() / (double) p.getTotal()) * expected.toMillis();
|
||||
var show = elapsed > 3000;
|
||||
if (!show) {
|
||||
return "...";
|
||||
}
|
||||
|
||||
var time = HumanReadableFormat.duration(expected) + " @ ";
|
||||
var progress = HumanReadableFormat.transferSpeed(
|
||||
model.getProgressTransferSpeed().getValue());
|
||||
return time + progress;
|
||||
},
|
||||
model.getProgressRemaining(),
|
||||
model.getProgressTransferSpeed(),
|
||||
model.getProgress());
|
||||
|
||||
var progressComp = new LabelComp(text)
|
||||
.styleClass("progress")
|
||||
.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT))
|
||||
.prefWidth(90)
|
||||
.prefWidth(140)
|
||||
.minWidth(Region.USE_PREF_SIZE);
|
||||
return progressComp;
|
||||
}
|
||||
@@ -106,10 +116,6 @@ public class BrowserStatusBarComp extends SimpleComp {
|
||||
return null;
|
||||
} else {
|
||||
var transferred = HumanReadableFormat.progressByteCount(p.getTransferred());
|
||||
if (!p.hasKnownTotalSize()) {
|
||||
return transferred;
|
||||
}
|
||||
|
||||
var all = HumanReadableFormat.byteCount(p.getTotal());
|
||||
return transferred + " / " + all;
|
||||
}
|
||||
|
||||
@@ -147,17 +147,6 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
|
||||
});
|
||||
}
|
||||
|
||||
private void refreshShowingState() {
|
||||
var sessions = TerminalView.get().getSessions();
|
||||
var remaining = sessions.stream()
|
||||
.filter(s -> terminalRequests.contains(s.getRequest())
|
||||
&& s.getTerminal().isRunning())
|
||||
.toList();
|
||||
if (remaining.isEmpty()) {
|
||||
((BrowserFullSessionModel) browserModel).unsplitTab(BrowserTerminalDockTabModel.this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (listener != null) {
|
||||
@@ -180,4 +169,15 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
|
||||
public DataStoreColor getColor() {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void refreshShowingState() {
|
||||
var sessions = TerminalView.get().getSessions();
|
||||
var remaining = sessions.stream()
|
||||
.filter(s -> terminalRequests.contains(s.getRequest())
|
||||
&& s.getTerminal().isRunning())
|
||||
.toList();
|
||||
if (remaining.isEmpty()) {
|
||||
((BrowserFullSessionModel) browserModel).unsplitTab(BrowserTerminalDockTabModel.this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ public class BrowserTransferComp extends SimpleComp {
|
||||
return Bindings.createStringBinding(
|
||||
() -> {
|
||||
var p = sourceItem.get().getProgress().getValue();
|
||||
if (p == null || !p.hasKnownTotalSize()) {
|
||||
if (p == null) {
|
||||
return entry.getFileName();
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ public class BrowserTransferComp extends SimpleComp {
|
||||
})
|
||||
.grow(false, true);
|
||||
var dragNotice = new LabelComp(AppI18n.observable("dragLocalFiles"))
|
||||
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2h-hand-left")))
|
||||
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2h-hand-back-left-outline")))
|
||||
.apply(struc -> struc.get().setWrapText(true))
|
||||
.hide(Bindings.or(model.getEmpty(), model.getTransferring()));
|
||||
|
||||
|
||||
@@ -149,14 +149,14 @@ public class BrowserTransferModel {
|
||||
progress -> {
|
||||
// Don't update item progress to keep it as finished
|
||||
if (progress == null) {
|
||||
itemModel.getProgress().setValue(null);
|
||||
itemModel.updateProgress(null);
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (item.getProgress()) {
|
||||
item.getProgress().setValue(progress);
|
||||
}
|
||||
itemModel.getProgress().setValue(progress);
|
||||
itemModel.updateProgress(progress);
|
||||
},
|
||||
itemModel.getTransferCancelled());
|
||||
var action = TransferFilesActionProvider.Action.builder()
|
||||
@@ -215,12 +215,12 @@ public class BrowserTransferModel {
|
||||
private Path getDownloadsTargetDirectory() throws Exception {
|
||||
var def = DesktopHelper.getDownloadsDirectory();
|
||||
var custom = AppPrefs.get().downloadsDirectory().getValue();
|
||||
if (custom == null || custom.isBlank()) {
|
||||
if (custom == null) {
|
||||
return def;
|
||||
}
|
||||
|
||||
try {
|
||||
var path = Path.of(custom);
|
||||
var path = custom.asLocalPath();
|
||||
if (Files.isDirectory(path)) {
|
||||
return path;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import lombok.Value;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
|
||||
@Value
|
||||
public class BrowserTransferProgress {
|
||||
@@ -12,32 +12,38 @@ public class BrowserTransferProgress {
|
||||
String name;
|
||||
long transferred;
|
||||
long total;
|
||||
Instant start;
|
||||
Instant timestamp = Instant.now();
|
||||
|
||||
public static BrowserTransferProgress finished(String name, long size) {
|
||||
return new BrowserTransferProgress(name, size, size, Instant.now());
|
||||
return new BrowserTransferProgress(name, size, size);
|
||||
}
|
||||
|
||||
public static long estimateTransferSpeed(BrowserTransferProgress start, BrowserTransferProgress end) {
|
||||
var diff = end.transferred - start.transferred;
|
||||
var duration = Duration.between(start.timestamp, end.timestamp);
|
||||
return (long) (diff / (duration.toMillis() / 1000.0));
|
||||
}
|
||||
|
||||
public static long estimateTransferSpeed(List<BrowserTransferProgress> list, BrowserTransferProgress now) {
|
||||
if (list.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var rSize = list.size() > 1 ? list.size() - 1 : list.size();
|
||||
var r = new double[rSize];
|
||||
for (int i = 0; i < rSize; i++) {
|
||||
r[i] = estimateTransferSpeed(list.get(i), now);
|
||||
}
|
||||
|
||||
double sum = 0;
|
||||
var lookBack = Math.min(r.length, 5);
|
||||
for (int i = 0; i < lookBack; i++) {
|
||||
sum += r[r.length - i - 1];
|
||||
}
|
||||
return (long) (sum / lookBack);
|
||||
}
|
||||
|
||||
public boolean done() {
|
||||
return transferred >= total;
|
||||
}
|
||||
|
||||
public boolean hasKnownTotalSize() {
|
||||
return total > 0;
|
||||
}
|
||||
|
||||
public Duration elapsedTime() {
|
||||
var now = Instant.now();
|
||||
var elapsed = Duration.between(start, now);
|
||||
return elapsed;
|
||||
}
|
||||
|
||||
public Duration expectedTimeRemaining() {
|
||||
var elapsed = elapsedTime();
|
||||
var share = (double) transferred / total;
|
||||
var rest = (1.0 - share) / share;
|
||||
var restMillis = (long) (elapsed.toMillis() * rest);
|
||||
var startupAdjustment = (long) (restMillis / (1.0 + Math.max(10000 - elapsed.toMillis(), 0) / 10000.0));
|
||||
return Duration.of(restMillis + startupAdjustment, ChronoUnit.MILLIS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,13 +20,6 @@ public abstract class BrowserIconDirectoryType {
|
||||
|
||||
private static final List<BrowserIconDirectoryType> ALL = new ArrayList<>();
|
||||
|
||||
public static synchronized BrowserIconDirectoryType byId(String id) {
|
||||
return ALL.stream()
|
||||
.filter(fileType -> fileType.getId().equals(id))
|
||||
.findAny()
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
public static synchronized void loadDefinitions() {
|
||||
ALL.add(new BrowserIconDirectoryType() {
|
||||
|
||||
@@ -47,7 +40,7 @@ public abstract class BrowserIconDirectoryType {
|
||||
}
|
||||
});
|
||||
|
||||
AppResources.with(AppResources.XPIPE_MODULE, "folder_list.txt", path -> {
|
||||
AppResources.with(AppResources.MAIN_MODULE, "folder_list.txt", path -> {
|
||||
try (var reader =
|
||||
new BufferedReader(new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
|
||||
@@ -25,7 +25,7 @@ public abstract class BrowserIconFileType {
|
||||
}
|
||||
|
||||
public static synchronized void loadDefinitions() {
|
||||
AppResources.with(AppResources.XPIPE_MODULE, "file_list.txt", path -> {
|
||||
AppResources.with(AppResources.MAIN_MODULE, "file_list.txt", path -> {
|
||||
try (var reader =
|
||||
new BufferedReader(new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
|
||||
@@ -7,10 +7,6 @@ public class BrowserIconVariant {
|
||||
private final String lightIcon;
|
||||
private final String darkIcon;
|
||||
|
||||
public BrowserIconVariant(String icon) {
|
||||
this(icon, icon);
|
||||
}
|
||||
|
||||
public BrowserIconVariant(String lightIcon, String darkIcon) {
|
||||
this.lightIcon = lightIcon;
|
||||
this.darkIcon = darkIcon;
|
||||
|
||||
@@ -2,13 +2,10 @@ package io.xpipe.app.browser.menu;
|
||||
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.util.LicenseProvider;
|
||||
|
||||
import javafx.scene.control.Menu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface BrowserMenuBranchProvider extends BrowserMenuItemProvider {
|
||||
@@ -36,12 +33,6 @@ public interface BrowserMenuBranchProvider extends BrowserMenuItemProvider {
|
||||
}
|
||||
m.setDisable(!isActive(model, selected));
|
||||
|
||||
if (getLicensedFeatureId() != null
|
||||
&& !LicenseProvider.get().getFeature(getLicensedFeatureId()).isSupported()) {
|
||||
m.setDisable(true);
|
||||
m.setGraphic(new FontIcon("mdi2p-professional-hexagon"));
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.comp.base.TooltipHelper;
|
||||
import io.xpipe.app.hub.action.StoreAction;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.BindingsHelper;
|
||||
import io.xpipe.app.util.LicenseProvider;
|
||||
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.MenuItem;
|
||||
@@ -19,7 +17,6 @@ import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -33,16 +30,6 @@ public interface BrowserMenuLeafProvider extends BrowserMenuItemProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
default boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
if (getDelegateActionProvider() != null) {
|
||||
var provider = BrowserActionProviders.forClass(getDelegateActionProvider());
|
||||
return provider.isApplicable(model, entries);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
default AbstractAction createAction(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var c = getDelegateActionProvider() != null
|
||||
@@ -106,24 +93,13 @@ public interface BrowserMenuLeafProvider extends BrowserMenuItemProvider {
|
||||
b.setDisable(!isActive(model, selected));
|
||||
});
|
||||
|
||||
if (getLicensedFeatureId() != null
|
||||
&& !LicenseProvider.get().getFeature(getLicensedFeatureId()).isSupported()) {
|
||||
b.setDisable(true);
|
||||
b.setGraphic(new FontIcon("mdi2p-professional-hexagon"));
|
||||
}
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
default MenuItem toMenuItem(BrowserFileSystemTabModel model, List<BrowserEntry> selected) {
|
||||
var name = getName(model, selected);
|
||||
var mi = new MenuItem();
|
||||
mi.textProperty().bind(BindingsHelper.map(name, s -> {
|
||||
if (getLicensedFeatureId() != null) {
|
||||
return LicenseProvider.get().getFeature(getLicensedFeatureId()).suffix(s);
|
||||
}
|
||||
return s;
|
||||
}));
|
||||
mi.textProperty().bind(name);
|
||||
mi.setOnAction(event -> {
|
||||
try {
|
||||
execute(model, selected);
|
||||
@@ -142,11 +118,16 @@ public interface BrowserMenuLeafProvider extends BrowserMenuItemProvider {
|
||||
mi.setMnemonicParsing(false);
|
||||
mi.setDisable(!isActive(model, selected));
|
||||
|
||||
if (getLicensedFeatureId() != null
|
||||
&& !LicenseProvider.get().getFeature(getLicensedFeatureId()).isSupported()) {
|
||||
mi.setDisable(true);
|
||||
}
|
||||
|
||||
return mi;
|
||||
}
|
||||
|
||||
@Override
|
||||
default boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
if (getDelegateActionProvider() != null) {
|
||||
var provider = BrowserActionProviders.forClass(getDelegateActionProvider());
|
||||
return provider.isApplicable(model, entries);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
package io.xpipe.app.browser.menu;
|
||||
|
||||
import io.xpipe.app.browser.action.impl.RunCommandInBackgroundActionProvider;
|
||||
import io.xpipe.app.browser.action.impl.RunCommandInBrowserActionProvider;
|
||||
import io.xpipe.app.browser.action.impl.RunCommandInTerminalActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.util.CommandDialog;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class MultiExecuteMenuProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
protected abstract CommandBuilder createCommand(
|
||||
ShellControl sc, BrowserFileSystemTabModel model, BrowserEntry entry);
|
||||
protected abstract List<CommandBuilder> createCommand(BrowserFileSystemTabModel model, List<BrowserEntry> entries);
|
||||
|
||||
@Override
|
||||
public List<BrowserMenuLeafProvider> getBranchingActions(
|
||||
@@ -25,25 +26,24 @@ public abstract class MultiExecuteMenuProvider implements BrowserMenuBranchProvi
|
||||
new BrowserMenuLeafProvider() {
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var sc = model.getFileSystem().getShell().orElseThrow();
|
||||
for (BrowserEntry entry : entries) {
|
||||
var c = createCommand(sc, model, entry);
|
||||
if (c == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var cmd = sc.command(c);
|
||||
model.openTerminalAsync(
|
||||
entry.getRawFileEntry().getName(),
|
||||
model.getCurrentDirectory() != null
|
||||
? model.getCurrentDirectory().getPath()
|
||||
: null,
|
||||
cmd,
|
||||
entries.size() == 1);
|
||||
var commands = createCommand(model, entries);
|
||||
for (CommandBuilder command : commands) {
|
||||
var builder = RunCommandInTerminalActionProvider.Action.builder();
|
||||
builder.initFiles(
|
||||
model, List.of(model.getCurrentPath().getValue()));
|
||||
builder.command(command.buildFull(
|
||||
model.getFileSystem().getShell().orElseThrow()));
|
||||
builder.build().executeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return AppPrefs.get().terminalType().getValue() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(
|
||||
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
@@ -52,29 +52,21 @@ public abstract class MultiExecuteMenuProvider implements BrowserMenuBranchProvi
|
||||
"executeInTerminal",
|
||||
t != null ? t.toTranslatedString().getValue() : "?");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return AppPrefs.get().terminalType().getValue() != null;
|
||||
}
|
||||
},
|
||||
new BrowserMenuLeafProvider() {
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
var sc = model.getFileSystem().getShell().orElseThrow();
|
||||
for (BrowserEntry entry : entries) {
|
||||
var c = createCommand(sc, model, entry);
|
||||
if (c == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var cmd = sc.command(c);
|
||||
CommandDialog.runAndShow(cmd);
|
||||
}
|
||||
model.refreshBrowserEntriesSync(entries);
|
||||
});
|
||||
var commands = createCommand(model, entries);
|
||||
for (CommandBuilder command : commands) {
|
||||
var builder = RunCommandInBrowserActionProvider.Action.builder();
|
||||
builder.initFiles(
|
||||
model, List.of(model.getCurrentPath().getValue()));
|
||||
builder.command(command.buildFull(
|
||||
model.getFileSystem().getShell().orElseThrow()));
|
||||
builder.build().executeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -86,22 +78,17 @@ public abstract class MultiExecuteMenuProvider implements BrowserMenuBranchProvi
|
||||
new BrowserMenuLeafProvider() {
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
var sc = model.getFileSystem().getShell().orElseThrow();
|
||||
for (BrowserEntry entry : entries) {
|
||||
var cmd = createCommand(sc, model, entry);
|
||||
if (cmd == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sc.command(cmd)
|
||||
.withWorkingDirectory(
|
||||
model.getCurrentDirectory().getPath())
|
||||
.execute();
|
||||
}
|
||||
model.refreshBrowserEntriesSync(entries);
|
||||
});
|
||||
var commands = createCommand(model, entries);
|
||||
for (CommandBuilder command : commands) {
|
||||
var builder = RunCommandInBackgroundActionProvider.Action.builder();
|
||||
builder.initFiles(
|
||||
model, List.of(model.getCurrentPath().getValue()));
|
||||
builder.command(command.buildFull(
|
||||
model.getFileSystem().getShell().orElseThrow()));
|
||||
builder.build().executeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
package io.xpipe.app.browser.menu;
|
||||
|
||||
import io.xpipe.app.browser.action.impl.RunCommandInBackgroundActionProvider;
|
||||
import io.xpipe.app.browser.action.impl.RunCommandInBrowserActionProvider;
|
||||
import io.xpipe.app.browser.action.impl.RunCommandInTerminalActionProvider;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class MultiExecuteSelectionMenuProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
protected abstract String createCommand(BrowserFileSystemTabModel model);
|
||||
|
||||
protected abstract String getTerminalTitle();
|
||||
|
||||
@Override
|
||||
public List<BrowserMenuLeafProvider> getBranchingActions(
|
||||
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return List.of(
|
||||
new BrowserMenuLeafProvider() {
|
||||
|
||||
@Override
|
||||
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var builder = RunCommandInTerminalActionProvider.Action.builder();
|
||||
builder.initEntries(model, entries);
|
||||
builder.title(getTerminalTitle());
|
||||
builder.command(createCommand(model));
|
||||
builder.build().executeAsync();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(
|
||||
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var t = AppPrefs.get().terminalType().getValue();
|
||||
return AppI18n.observable(
|
||||
"executeInTerminal",
|
||||
t != null ? t.toTranslatedString().getValue() : "?");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return AppPrefs.get().terminalType().getValue() != null;
|
||||
}
|
||||
},
|
||||
new BrowserMenuLeafProvider() {
|
||||
|
||||
@Override
|
||||
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var builder = RunCommandInBrowserActionProvider.Action.builder();
|
||||
builder.initEntries(model, entries);
|
||||
builder.command(createCommand(model));
|
||||
builder.build().executeAsync();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(
|
||||
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return AppI18n.observable("runInFileBrowser");
|
||||
}
|
||||
},
|
||||
new BrowserMenuLeafProvider() {
|
||||
|
||||
@Override
|
||||
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var builder = RunCommandInBackgroundActionProvider.Action.builder();
|
||||
builder.initEntries(model, entries);
|
||||
builder.command(createCommand(model));
|
||||
builder.build().executeAsync();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(
|
||||
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return AppI18n.observable("runSilent");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -23,13 +23,18 @@ public class BackMenuProvider implements BrowserMenuLeafProvider {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return "back";
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return new LabelGraphic.IconGraphic("fth-arrow-left");
|
||||
return new LabelGraphic.IconGraphic("mdi2a-arrow-left");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -42,11 +47,6 @@ public class BackMenuProvider implements BrowserMenuLeafProvider {
|
||||
return AppI18n.observable("back");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return model.getHistory().canGoBackProperty().get();
|
||||
|
||||
+8
-8
@@ -26,16 +26,16 @@ public class BrowseInNativeManagerMenuProvider implements BrowserMenuLeafProvide
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean acceptsEmptySelection() {
|
||||
return true;
|
||||
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return switch (OsType.getLocal()) {
|
||||
case OsType.Windows ignored -> AppI18n.observable("browseInWindowsExplorer");
|
||||
case OsType.Linux ignored -> AppI18n.observable("browseInDefaultFileManager");
|
||||
case OsType.MacOs ignored -> AppI18n.observable("browseInFinder");
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return switch (OsType.getLocal()) {
|
||||
case OsType.Windows windows -> AppI18n.observable("browseInWindowsExplorer");
|
||||
case OsType.Linux linux -> AppI18n.observable("browseInDefaultFileManager");
|
||||
case OsType.MacOs macOs -> AppI18n.observable("browseInFinder");
|
||||
};
|
||||
public boolean acceptsEmptySelection() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,20 @@ import java.util.stream.Stream;
|
||||
|
||||
public class ChgrpMenuProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
private static List<BrowserMenuItemProvider> getLeafActions(BrowserFileSystemTabModel model, boolean recursive) {
|
||||
List<BrowserMenuItemProvider> actions = Stream.<BrowserMenuItemProvider>concat(
|
||||
model.getCache().getGroups().entrySet().stream()
|
||||
.filter(e -> !e.getValue().equals("nohome")
|
||||
&& !e.getValue().equals("nogroup")
|
||||
&& !e.getValue().equals("nobody")
|
||||
&& (e.getKey().equals(0) || e.getKey() >= 900))
|
||||
.map(e -> e.getValue())
|
||||
.map(s -> (BrowserMenuLeafProvider) new FixedProvider(s, recursive)),
|
||||
Stream.of(new CustomProvider(recursive)))
|
||||
.toList();
|
||||
return actions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return new LabelGraphic.IconGraphic("mdi2a-account-group-outline");
|
||||
@@ -93,20 +107,6 @@ public class ChgrpMenuProvider implements BrowserMenuBranchProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private static List<BrowserMenuItemProvider> getLeafActions(BrowserFileSystemTabModel model, boolean recursive) {
|
||||
List<BrowserMenuItemProvider> actions = Stream.<BrowserMenuItemProvider>concat(
|
||||
model.getCache().getGroups().entrySet().stream()
|
||||
.filter(e -> !e.getValue().equals("nohome")
|
||||
&& !e.getValue().equals("nogroup")
|
||||
&& !e.getValue().equals("nobody")
|
||||
&& (e.getKey().equals(0) || e.getKey() >= 900))
|
||||
.map(e -> e.getValue())
|
||||
.map(s -> (BrowserMenuLeafProvider) new FixedProvider(s, recursive)),
|
||||
Stream.of(new CustomProvider(recursive)))
|
||||
.toList();
|
||||
return actions;
|
||||
}
|
||||
|
||||
private static class FixedProvider implements BrowserMenuLeafProvider {
|
||||
|
||||
private final String group;
|
||||
|
||||
@@ -22,6 +22,20 @@ import java.util.List;
|
||||
|
||||
public class ChmodMenuProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
private static List<BrowserMenuItemProvider> getLeafActions(BrowserFileSystemTabModel model, boolean recursive) {
|
||||
var custom = new CustomProvider(recursive);
|
||||
return List.of(
|
||||
new FixedProvider("400", recursive),
|
||||
new FixedProvider("600", recursive),
|
||||
new FixedProvider("644", recursive),
|
||||
new FixedProvider("700", recursive),
|
||||
new FixedProvider("755", recursive),
|
||||
new FixedProvider("777", recursive),
|
||||
new FixedProvider("u+x", recursive),
|
||||
new FixedProvider("a+x", recursive),
|
||||
custom);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return new LabelGraphic.IconGraphic("mdi2w-wrench-outline");
|
||||
@@ -91,20 +105,6 @@ public class ChmodMenuProvider implements BrowserMenuBranchProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private static List<BrowserMenuItemProvider> getLeafActions(BrowserFileSystemTabModel model, boolean recursive) {
|
||||
var custom = new CustomProvider(recursive);
|
||||
return List.of(
|
||||
new FixedProvider("400", recursive),
|
||||
new FixedProvider("600", recursive),
|
||||
new FixedProvider("644", recursive),
|
||||
new FixedProvider("700", recursive),
|
||||
new FixedProvider("755", recursive),
|
||||
new FixedProvider("777", recursive),
|
||||
new FixedProvider("u+x", recursive),
|
||||
new FixedProvider("a+x", recursive),
|
||||
custom);
|
||||
}
|
||||
|
||||
private static class FixedProvider implements BrowserMenuLeafProvider {
|
||||
|
||||
private final String permissions;
|
||||
|
||||
@@ -23,6 +23,19 @@ import java.util.stream.Stream;
|
||||
|
||||
public class ChownMenuProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
private static List<BrowserMenuItemProvider> getLeafActions(BrowserFileSystemTabModel model, boolean recursive) {
|
||||
var actions = Stream.<BrowserMenuItemProvider>concat(
|
||||
model.getCache().getUsers().entrySet().stream()
|
||||
.filter(e -> !e.getValue().equals("nohome")
|
||||
&& !e.getValue().equals("nobody")
|
||||
&& (e.getKey().equals(0) || e.getKey() >= 900))
|
||||
.map(e -> e.getValue())
|
||||
.map(s -> (BrowserMenuLeafProvider) new FixedProvider(s, recursive)),
|
||||
Stream.of(new CustomProvider(recursive)))
|
||||
.toList();
|
||||
return actions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return new LabelGraphic.IconGraphic("mdi2a-account-edit");
|
||||
@@ -93,19 +106,6 @@ public class ChownMenuProvider implements BrowserMenuBranchProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private static List<BrowserMenuItemProvider> getLeafActions(BrowserFileSystemTabModel model, boolean recursive) {
|
||||
var actions = Stream.<BrowserMenuItemProvider>concat(
|
||||
model.getCache().getUsers().entrySet().stream()
|
||||
.filter(e -> !e.getValue().equals("nohome")
|
||||
&& !e.getValue().equals("nobody")
|
||||
&& (e.getKey().equals(0) || e.getKey() >= 900))
|
||||
.map(e -> e.getValue())
|
||||
.map(s -> (BrowserMenuLeafProvider) new FixedProvider(s, recursive)),
|
||||
Stream.of(new CustomProvider(recursive)))
|
||||
.toList();
|
||||
return actions;
|
||||
}
|
||||
|
||||
private static class FixedProvider implements BrowserMenuLeafProvider {
|
||||
|
||||
private final String owner;
|
||||
|
||||
+14
-14
@@ -16,13 +16,6 @@ import java.util.List;
|
||||
|
||||
public class ComputeDirectorySizesMenuProvider implements BrowserMenuLeafProvider {
|
||||
|
||||
@Override
|
||||
public AbstractAction createAction(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var builder = ComputeDirectorySizesActionProvider.Action.builder();
|
||||
builder.initEntries(model, entries);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return "computeDirectorySizes";
|
||||
}
|
||||
@@ -32,6 +25,11 @@ public class ComputeDirectorySizesMenuProvider implements BrowserMenuLeafProvide
|
||||
return new LabelGraphic.IconGraphic("mdi2f-format-list-text");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BrowserMenuCategory getCategory() {
|
||||
return BrowserMenuCategory.ACTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var topLevel =
|
||||
@@ -39,6 +37,11 @@ public class ComputeDirectorySizesMenuProvider implements BrowserMenuLeafProvide
|
||||
return AppI18n.observable(topLevel ? "computeDirectorySizes" : "computeSize");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean acceptsEmptySelection() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.stream()
|
||||
@@ -46,12 +49,9 @@ public class ComputeDirectorySizesMenuProvider implements BrowserMenuLeafProvide
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean acceptsEmptySelection() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BrowserMenuCategory getCategory() {
|
||||
return BrowserMenuCategory.ACTION;
|
||||
public AbstractAction createAction(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var builder = ComputeDirectorySizesActionProvider.Action.builder();
|
||||
builder.initEntries(model, entries);
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,13 +37,13 @@ public class CopyMenuProvider implements BrowserMenuLeafProvider {
|
||||
return new KeyCodeCombination(KeyCode.C, KeyCombination.SHORTCUT_DOWN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean acceptsEmptySelection() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return AppI18n.observable("copy");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean acceptsEmptySelection() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,14 +21,27 @@ import java.util.stream.Collectors;
|
||||
|
||||
public class CopyPathMenuProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
@Override
|
||||
public BrowserMenuCategory getCategory() {
|
||||
return BrowserMenuCategory.COPY_PASTE;
|
||||
private static String centerEllipsis(String input, int length) {
|
||||
if (input == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (input.length() <= length) {
|
||||
return input;
|
||||
}
|
||||
|
||||
var half = (length / 2) - 5;
|
||||
return input.substring(0, half) + " ... " + input.substring(input.length() - half);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean acceptsEmptySelection() {
|
||||
return true;
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return new LabelGraphic.IconGraphic("mdi2c-content-copy");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BrowserMenuCategory getCategory() {
|
||||
return BrowserMenuCategory.COPY_PASTE;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -37,8 +50,8 @@ public class CopyPathMenuProvider implements BrowserMenuBranchProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return new LabelGraphic.IconGraphic("mdi2c-content-copy");
|
||||
public boolean acceptsEmptySelection() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -75,6 +88,11 @@ public class CopyPathMenuProvider implements BrowserMenuBranchProvider {
|
||||
}
|
||||
},
|
||||
new BrowserMenuLeafProvider() {
|
||||
@Override
|
||||
public boolean automaticallyResolveLinks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(
|
||||
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
@@ -90,18 +108,6 @@ public class CopyPathMenuProvider implements BrowserMenuBranchProvider {
|
||||
return AppI18n.observable("absoluteLinkPaths");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.stream()
|
||||
.allMatch(browserEntry ->
|
||||
browserEntry.getRawFileEntry().getKind() == FileKind.LINK);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean automaticallyResolveLinks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var s = entries.stream()
|
||||
@@ -109,6 +115,13 @@ public class CopyPathMenuProvider implements BrowserMenuBranchProvider {
|
||||
.collect(Collectors.joining("\n"));
|
||||
ClipboardHelper.copyText(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.stream()
|
||||
.allMatch(browserEntry ->
|
||||
browserEntry.getRawFileEntry().getKind() == FileKind.LINK);
|
||||
}
|
||||
},
|
||||
new BrowserMenuLeafProvider() {
|
||||
@Override
|
||||
@@ -121,21 +134,12 @@ public class CopyPathMenuProvider implements BrowserMenuBranchProvider {
|
||||
.getRawFileEntry()
|
||||
.getPath()
|
||||
.toString(),
|
||||
50)
|
||||
+ "\"");
|
||||
50) + "\"");
|
||||
}
|
||||
|
||||
return AppI18n.observable("absolutePathsQuoted");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.stream().anyMatch(entry -> entry.getRawFileEntry()
|
||||
.getPath()
|
||||
.toString()
|
||||
.contains(" "));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var s = entries.stream()
|
||||
@@ -143,6 +147,14 @@ public class CopyPathMenuProvider implements BrowserMenuBranchProvider {
|
||||
.collect(Collectors.joining("\n"));
|
||||
ClipboardHelper.copyText(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.stream().anyMatch(entry -> entry.getRawFileEntry()
|
||||
.getPath()
|
||||
.toString()
|
||||
.contains(" "));
|
||||
}
|
||||
},
|
||||
new BrowserMenuLeafProvider() {
|
||||
@Override
|
||||
@@ -175,6 +187,11 @@ public class CopyPathMenuProvider implements BrowserMenuBranchProvider {
|
||||
}
|
||||
},
|
||||
new BrowserMenuLeafProvider() {
|
||||
@Override
|
||||
public boolean automaticallyResolveLinks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(
|
||||
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
@@ -190,6 +207,14 @@ public class CopyPathMenuProvider implements BrowserMenuBranchProvider {
|
||||
return AppI18n.observable("linkFileNames");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var s = entries.stream()
|
||||
.map(entry -> entry.getRawFileEntry().getPath().getFileName())
|
||||
.collect(Collectors.joining("\n"));
|
||||
ClipboardHelper.copyText(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.stream()
|
||||
@@ -203,19 +228,6 @@ public class CopyPathMenuProvider implements BrowserMenuBranchProvider {
|
||||
.getPath()
|
||||
.getFileName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean automaticallyResolveLinks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var s = entries.stream()
|
||||
.map(entry -> entry.getRawFileEntry().getPath().getFileName())
|
||||
.collect(Collectors.joining("\n"));
|
||||
ClipboardHelper.copyText(s);
|
||||
}
|
||||
},
|
||||
new BrowserMenuLeafProvider() {
|
||||
@Override
|
||||
@@ -228,21 +240,12 @@ public class CopyPathMenuProvider implements BrowserMenuBranchProvider {
|
||||
.getRawFileEntry()
|
||||
.getPath()
|
||||
.getFileName(),
|
||||
50)
|
||||
+ "\"");
|
||||
50) + "\"");
|
||||
}
|
||||
|
||||
return AppI18n.observable("fileNamesQuoted");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.stream().anyMatch(entry -> entry.getRawFileEntry()
|
||||
.getPath()
|
||||
.getFileName()
|
||||
.contains(" "));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var s = entries.stream()
|
||||
@@ -251,19 +254,14 @@ public class CopyPathMenuProvider implements BrowserMenuBranchProvider {
|
||||
.collect(Collectors.joining("\n"));
|
||||
ClipboardHelper.copyText(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.stream().anyMatch(entry -> entry.getRawFileEntry()
|
||||
.getPath()
|
||||
.getFileName()
|
||||
.contains(" "));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static String centerEllipsis(String input, int length) {
|
||||
if (input == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (input.length() <= length) {
|
||||
return input;
|
||||
}
|
||||
|
||||
var half = (length / 2) - 5;
|
||||
return input.substring(0, half) + " ... " + input.substring(input.length() - half);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,11 @@ public class DeleteMenuProvider implements BrowserMenuLeafProvider {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean automaticallyResolveLinks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return new LabelGraphic.IconGraphic("mdi2d-delete");
|
||||
@@ -48,11 +53,6 @@ public class DeleteMenuProvider implements BrowserMenuLeafProvider {
|
||||
return new KeyCodeCombination(KeyCode.DELETE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean automaticallyResolveLinks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return AppI18n.observable(
|
||||
|
||||
@@ -27,6 +27,15 @@ public class DownloadMenuProvider implements BrowserMenuLeafProvider {
|
||||
fullSessionModel.getLocalTransfersStage().drop(model, entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var transfer = model.getBrowserModel();
|
||||
if (!(transfer instanceof BrowserFullSessionModel)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return "download";
|
||||
}
|
||||
@@ -50,13 +59,4 @@ public class DownloadMenuProvider implements BrowserMenuLeafProvider {
|
||||
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return AppI18n.observable("download");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var transfer = model.getBrowserModel();
|
||||
if (!(transfer instanceof BrowserFullSessionModel)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,6 @@ import java.util.List;
|
||||
|
||||
public class EditFileMenuProvider implements BrowserMenuLeafProvider {
|
||||
|
||||
@Override
|
||||
public KeyCombination getShortcut() {
|
||||
return new KeyCodeCombination(KeyCode.E, KeyCombination.SHORTCUT_DOWN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
@@ -34,6 +29,11 @@ public class EditFileMenuProvider implements BrowserMenuLeafProvider {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return new LabelGraphic.IconGraphic("mdi2p-pencil");
|
||||
@@ -44,6 +44,11 @@ public class EditFileMenuProvider implements BrowserMenuLeafProvider {
|
||||
return BrowserMenuCategory.OPEN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyCombination getShortcut() {
|
||||
return new KeyCodeCombination(KeyCode.E, KeyCombination.SHORTCUT_DOWN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var e = AppPrefs.get().externalEditor().getValue();
|
||||
@@ -51,11 +56,6 @@ public class EditFileMenuProvider implements BrowserMenuLeafProvider {
|
||||
"editWithEditor", e != null ? e.toTranslatedString().getValue() : "?");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var e = AppPrefs.get().externalEditor().getValue();
|
||||
|
||||
@@ -20,6 +20,19 @@ public class FollowLinkMenuProvider implements BrowserMenuLeafProvider {
|
||||
model.cdAsync(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.size() == 1
|
||||
&& entries.stream()
|
||||
.allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.LINK
|
||||
&& entry.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean automaticallyResolveLinks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return new LabelGraphic.IconGraphic("mdi2a-arrow-top-right-thick");
|
||||
@@ -34,17 +47,4 @@ public class FollowLinkMenuProvider implements BrowserMenuLeafProvider {
|
||||
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return AppI18n.observable("followLink");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.size() == 1
|
||||
&& entries.stream()
|
||||
.allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.LINK
|
||||
&& entry.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean automaticallyResolveLinks() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,13 +23,18 @@ public class ForwardMenuProvider implements BrowserMenuLeafProvider {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return "forward";
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return new LabelGraphic.IconGraphic("fth-arrow-right");
|
||||
return new LabelGraphic.IconGraphic("mdi2a-arrow-right");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -42,11 +47,6 @@ public class ForwardMenuProvider implements BrowserMenuLeafProvider {
|
||||
return AppI18n.observable("goForward");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return model.getHistory().canGoForthProperty().get();
|
||||
|
||||
@@ -3,12 +3,8 @@ package io.xpipe.app.browser.menu.impl;
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.browser.icon.BrowserIconFileType;
|
||||
import io.xpipe.app.browser.menu.BrowserApplicationPathMenuProvider;
|
||||
import io.xpipe.app.browser.menu.BrowserMenuCategory;
|
||||
import io.xpipe.app.browser.menu.FileTypeMenuProvider;
|
||||
import io.xpipe.app.browser.menu.MultiExecuteMenuProvider;
|
||||
import io.xpipe.app.browser.menu.*;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
@@ -34,13 +30,6 @@ public class JarMenuProvider extends MultiExecuteMenuProvider
|
||||
return super.isApplicable(model, entries) && FileTypeMenuProvider.super.isApplicable(model, entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder createCommand(ShellControl sc, BrowserFileSystemTabModel model, BrowserEntry entry) {
|
||||
return CommandBuilder.of()
|
||||
.add("java", "-jar")
|
||||
.addFile(entry.getRawFileEntry().getPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BrowserIconFileType getType() {
|
||||
return BrowserIconFileType.byId("jar");
|
||||
@@ -50,4 +39,15 @@ public class JarMenuProvider extends MultiExecuteMenuProvider
|
||||
public String getExecutable() {
|
||||
return "java";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<CommandBuilder> createCommand(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.stream()
|
||||
.map(browserEntry -> {
|
||||
return CommandBuilder.of()
|
||||
.add("java", "-jar")
|
||||
.addFile(browserEntry.getRawFileEntry().getPath());
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,13 +37,13 @@ public class NewItemMenuProvider implements BrowserMenuBranchProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean acceptsEmptySelection() {
|
||||
return true;
|
||||
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return AppI18n.observable("new");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return AppI18n.observable("new");
|
||||
public boolean acceptsEmptySelection() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -186,6 +186,11 @@ public class NewItemMenuProvider implements BrowserMenuBranchProvider {
|
||||
modal.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return new LabelGraphic.CompGraphic(BrowserIcons.createDefaultFileIcon());
|
||||
@@ -196,11 +201,6 @@ public class NewItemMenuProvider implements BrowserMenuBranchProvider {
|
||||
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return AppI18n.observable("symbolicLink");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -26,6 +26,13 @@ public class OpenDirectoryInNewTabMenuProvider implements BrowserMenuLeafProvide
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return model.getBrowserModel() instanceof BrowserFullSessionModel
|
||||
&& entries.size() == 1
|
||||
&& entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return new LabelGraphic.IconGraphic("mdi2f-folder-open-outline");
|
||||
@@ -41,20 +48,13 @@ public class OpenDirectoryInNewTabMenuProvider implements BrowserMenuLeafProvide
|
||||
return new KeyCodeCombination(KeyCode.ENTER, KeyCombination.SHIFT_DOWN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean acceptsEmptySelection() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return AppI18n.observable("openInNewTab");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return model.getBrowserModel() instanceof BrowserFullSessionModel
|
||||
&& entries.size() == 1
|
||||
&& entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY);
|
||||
public boolean acceptsEmptySelection() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,13 @@ public class OpenFileWithMenuProvider implements BrowserMenuLeafProvider {
|
||||
return OpenFileWithActionProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return OsType.getLocal() == OsType.WINDOWS
|
||||
&& entries.size() == 1
|
||||
&& entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return new LabelGraphic.IconGraphic("mdi2b-book-open-page-variant-outline");
|
||||
@@ -44,11 +51,4 @@ public class OpenFileWithMenuProvider implements BrowserMenuLeafProvider {
|
||||
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return AppI18n.observable("openFileWith");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return OsType.getLocal().equals(OsType.WINDOWS)
|
||||
&& entries.size() == 1
|
||||
&& entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -22,23 +22,23 @@ public class OpenNativeFileDetailsMenuProvider implements BrowserMenuLeafProvide
|
||||
return OpenFileNativeDetailsActionProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyCombination getShortcut() {
|
||||
return new KeyCodeCombination(KeyCode.ENTER, KeyCombination.ALT_DOWN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BrowserMenuCategory getCategory() {
|
||||
return BrowserMenuCategory.OPEN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean acceptsEmptySelection() {
|
||||
return true;
|
||||
public KeyCombination getShortcut() {
|
||||
return new KeyCodeCombination(KeyCode.ENTER, KeyCombination.ALT_DOWN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return AppI18n.observable("showDetails");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean acceptsEmptySelection() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+10
-10
@@ -22,11 +22,6 @@ import java.util.List;
|
||||
|
||||
public class OpenTerminalInDirectoryMenuProvider implements BrowserMenuLeafProvider {
|
||||
|
||||
@Override
|
||||
public Class<? extends BrowserActionProvider> getDelegateActionProvider() {
|
||||
return OpenTerminalActionProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var dirs = entries.size() > 0
|
||||
@@ -42,6 +37,16 @@ public class OpenTerminalInDirectoryMenuProvider implements BrowserMenuLeafProvi
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends BrowserActionProvider> getDelegateActionProvider() {
|
||||
return OpenTerminalActionProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return "openInTerminal";
|
||||
}
|
||||
@@ -66,11 +71,6 @@ public class OpenTerminalInDirectoryMenuProvider implements BrowserMenuLeafProvi
|
||||
return AppI18n.observable("openInTerminal");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var t = AppPrefs.get().terminalType().getValue();
|
||||
|
||||
@@ -42,6 +42,14 @@ public class PasteMenuProvider implements BrowserMenuLeafProvider {
|
||||
BrowserFileTransferMode.COPY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return (entries.size() == 1
|
||||
&& entries.stream()
|
||||
.allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY))
|
||||
|| entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return new LabelGraphic.IconGraphic("mdi2c-content-paste");
|
||||
@@ -57,22 +65,14 @@ public class PasteMenuProvider implements BrowserMenuLeafProvider {
|
||||
return new KeyCodeCombination(KeyCode.V, KeyCombination.SHORTCUT_DOWN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean acceptsEmptySelection() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return AppI18n.observable("paste");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return (entries.size() == 1
|
||||
&& entries.stream()
|
||||
.allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY))
|
||||
|| entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE);
|
||||
public boolean acceptsEmptySelection() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -23,6 +23,11 @@ public class RefreshDirectoryMenuProvider implements BrowserMenuLeafProvider {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return "refresh";
|
||||
}
|
||||
@@ -42,11 +47,6 @@ public class RefreshDirectoryMenuProvider implements BrowserMenuLeafProvider {
|
||||
return AppI18n.observable("refresh");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return !model.getInOverview().get();
|
||||
|
||||
@@ -22,6 +22,16 @@ public class RenameMenuProvider implements BrowserMenuLeafProvider {
|
||||
model.getFileList().getEditing().setValue(entries.getFirst());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.size() == 1 && entries.getFirst().getRawFileEntry().getKind() != FileKind.LINK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean automaticallyResolveLinks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return new LabelGraphic.IconGraphic("mdi2r-rename-box");
|
||||
@@ -42,16 +52,6 @@ public class RenameMenuProvider implements BrowserMenuLeafProvider {
|
||||
return AppI18n.observable("rename");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.size() == 1 && entries.getFirst().getRawFileEntry().getKind() != FileKind.LINK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean automaticallyResolveLinks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "renameFile";
|
||||
|
||||
@@ -7,7 +7,6 @@ import io.xpipe.app.browser.menu.MultiExecuteMenuProvider;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.FileEntry;
|
||||
import io.xpipe.app.process.CommandBuilder;
|
||||
import io.xpipe.app.process.ShellControl;
|
||||
import io.xpipe.app.process.ShellDialects;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
import io.xpipe.core.FileKind;
|
||||
@@ -25,17 +24,17 @@ public class RunFileMenuProvider extends MultiExecuteMenuProvider {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.getInfo() != null && e.getInfo().possiblyExecutable()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var shell = e.getFileSystem().getShell();
|
||||
if (shell.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var os = shell.get().getOsType();
|
||||
if (os.equals(OsType.WINDOWS)
|
||||
|
||||
if (e.getInfo() != null && e.getInfo().possiblyExecutable() && os != OsType.WINDOWS) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (os == OsType.WINDOWS
|
||||
&& Stream.of("exe", "bat", "ps1", "cmd")
|
||||
.anyMatch(s -> e.getPath().toString().endsWith(s))) {
|
||||
return true;
|
||||
@@ -73,9 +72,20 @@ public class RunFileMenuProvider extends MultiExecuteMenuProvider {
|
||||
return entries.stream().allMatch(entry -> isExecutable(entry.getRawFileEntry()));
|
||||
}
|
||||
|
||||
protected CommandBuilder createCommand(ShellControl sc, BrowserFileSystemTabModel model, BrowserEntry entry) {
|
||||
return CommandBuilder.of()
|
||||
.add(sc.getShellDialect()
|
||||
.runScriptCommand(sc, entry.getRawFileEntry().getPath().toString()));
|
||||
@Override
|
||||
protected List<CommandBuilder> createCommand(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var sc = model.getFileSystem().getShell().orElseThrow();
|
||||
return entries.stream()
|
||||
.map(browserEntry -> {
|
||||
return CommandBuilder.of()
|
||||
.add(sc.getShellDialect()
|
||||
.runScriptCommand(
|
||||
sc,
|
||||
browserEntry
|
||||
.getRawFileEntry()
|
||||
.getPath()
|
||||
.toString()));
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
+21
-21
@@ -32,20 +32,6 @@ public class BaseUntarMenuProvider implements BrowserApplicationPathMenuProvider
|
||||
return new LabelGraphic.CompGraphic(BrowserIcons.createContextMenuIcon(BrowserIconFileType.byId("zip")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExecutable() {
|
||||
return "tar";
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractAction createAction(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var builder = UntarActionProvider.Action.builder();
|
||||
builder.initEntries(model, entries);
|
||||
builder.gz(gz);
|
||||
builder.toDirectory(toDirectory);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BrowserMenuCategory getCategory() {
|
||||
return BrowserMenuCategory.CUSTOM;
|
||||
@@ -61,13 +47,6 @@ public class BaseUntarMenuProvider implements BrowserApplicationPathMenuProvider
|
||||
return toDirectory ? AppI18n.observable("untarDirectory", dir) : AppI18n.observable("untarHere");
|
||||
}
|
||||
|
||||
private FilePath getTarget(FilePath name) {
|
||||
return FilePath.of(name.toString()
|
||||
.replaceAll("\\.tar$", "")
|
||||
.replaceAll("\\.tar.gz$", "")
|
||||
.replaceAll("\\.tgz$", ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
if (gz) {
|
||||
@@ -82,4 +61,25 @@ public class BaseUntarMenuProvider implements BrowserApplicationPathMenuProvider
|
||||
return entries.stream()
|
||||
.allMatch(entry -> entry.getRawFileEntry().getPath().toString().endsWith(".tar"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExecutable() {
|
||||
return "tar";
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractAction createAction(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var builder = UntarActionProvider.Action.builder();
|
||||
builder.initEntries(model, entries);
|
||||
builder.gz(gz);
|
||||
builder.toDirectory(toDirectory);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private FilePath getTarget(FilePath name) {
|
||||
return FilePath.of(name.toString()
|
||||
.replaceAll("\\.tar$", "")
|
||||
.replaceAll("\\.tar.gz$", "")
|
||||
.replaceAll("\\.tgz$", ""));
|
||||
}
|
||||
}
|
||||
|
||||
+14
-14
@@ -30,19 +30,6 @@ public abstract class BaseUnzipUnixMenuProvider implements BrowserMenuLeafProvid
|
||||
return new LabelGraphic.CompGraphic(BrowserIcons.createContextMenuIcon(BrowserIconFileType.byId("zip")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExecutable() {
|
||||
return "unzip";
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractAction createAction(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var builder = UnzipActionProvider.Action.builder();
|
||||
builder.initEntries(model, entries);
|
||||
builder.toDirectory(toDirectory);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BrowserMenuCategory getCategory() {
|
||||
return BrowserMenuCategory.CUSTOM;
|
||||
@@ -61,11 +48,24 @@ public abstract class BaseUnzipUnixMenuProvider implements BrowserMenuLeafProvid
|
||||
return toDirectory ? AppI18n.observable("unzipDirectory", dir) : AppI18n.observable("unzipHere");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExecutable() {
|
||||
return "unzip";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.stream()
|
||||
.allMatch(entry ->
|
||||
entry.getRawFileEntry().getPath().toString().endsWith(".zip"))
|
||||
&& !model.getFileSystem().getShell().orElseThrow().getOsType().equals(OsType.WINDOWS);
|
||||
&& model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractAction createAction(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var builder = UnzipActionProvider.Action.builder();
|
||||
builder.initEntries(model, entries);
|
||||
builder.toDirectory(toDirectory);
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -47,6 +47,14 @@ public abstract class BaseUnzipWindowsActionProvider implements BrowserMenuLeafP
|
||||
return toDirectory ? AppI18n.observable("unzipDirectory", dir) : AppI18n.observable("unzipHere");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.stream()
|
||||
.allMatch(entry ->
|
||||
entry.getRawFileEntry().getPath().toString().endsWith(".zip"))
|
||||
&& model.getFileSystem().getShell().orElseThrow().getOsType() == OsType.WINDOWS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractAction createAction(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
var builder = UnzipActionProvider.Action.builder();
|
||||
@@ -54,12 +62,4 @@ public abstract class BaseUnzipWindowsActionProvider implements BrowserMenuLeafP
|
||||
builder.toDirectory(toDirectory);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return entries.stream()
|
||||
.allMatch(entry ->
|
||||
entry.getRawFileEntry().getPath().toString().endsWith(".zip"))
|
||||
&& model.getFileSystem().getShell().orElseThrow().getOsType().equals(OsType.WINDOWS);
|
||||
}
|
||||
}
|
||||
|
||||
+47
-47
@@ -73,62 +73,21 @@ public class CompressMenuProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
return List.of(
|
||||
new ZipActionProvider(false),
|
||||
new TarBasedActionProvider(false, false) {
|
||||
@Override
|
||||
protected String getExtension() {
|
||||
return "tar";
|
||||
}
|
||||
},
|
||||
new TarBasedActionProvider(false, true) {
|
||||
|
||||
@Override
|
||||
protected String getExtension() {
|
||||
return "tar.gz";
|
||||
}
|
||||
},
|
||||
new TarBasedActionProvider(false, false) {
|
||||
@Override
|
||||
protected String getExtension() {
|
||||
return "tar";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private class BranchProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
private final boolean directory;
|
||||
|
||||
private BranchProvider(boolean directory) {
|
||||
this.directory = directory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return AppI18n.observable(directory ? "excludeRoot" : "includeRoot");
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return directory
|
||||
? new LabelGraphic.IconGraphic("mdi2f-file-tree")
|
||||
: new LabelGraphic.IconGraphic("mdi2f-file-outline");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends BrowserMenuItemProvider> getBranchingActions(
|
||||
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return List.of(
|
||||
new ZipActionProvider(directory),
|
||||
new TarBasedActionProvider(directory, false) {
|
||||
@Override
|
||||
protected String getExtension() {
|
||||
return "tar";
|
||||
}
|
||||
},
|
||||
new TarBasedActionProvider(directory, true) {
|
||||
|
||||
@Override
|
||||
protected String getExtension() {
|
||||
return "tar.gz";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private abstract static class LeafProvider implements BrowserMenuLeafProvider {
|
||||
|
||||
protected final boolean directory;
|
||||
@@ -173,6 +132,47 @@ public class CompressMenuProvider implements BrowserMenuBranchProvider {
|
||||
protected abstract String getExtension();
|
||||
}
|
||||
|
||||
private class BranchProvider implements BrowserMenuBranchProvider {
|
||||
|
||||
private final boolean directory;
|
||||
|
||||
private BranchProvider(boolean directory) {
|
||||
this.directory = directory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return directory
|
||||
? new LabelGraphic.IconGraphic("mdi2f-file-tree")
|
||||
: new LabelGraphic.IconGraphic("mdi2f-file-outline");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return AppI18n.observable(directory ? "excludeRoot" : "includeRoot");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends BrowserMenuItemProvider> getBranchingActions(
|
||||
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
return List.of(
|
||||
new ZipActionProvider(directory),
|
||||
new TarBasedActionProvider(directory, true) {
|
||||
|
||||
@Override
|
||||
protected String getExtension() {
|
||||
return "tar.gz";
|
||||
}
|
||||
},
|
||||
new TarBasedActionProvider(directory, false) {
|
||||
@Override
|
||||
protected String getExtension() {
|
||||
return "tar";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private class ZipActionProvider extends LeafProvider {
|
||||
|
||||
private ZipActionProvider(boolean directory) {
|
||||
|
||||
@@ -12,6 +12,11 @@ import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
public class TarActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "tar";
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
@@ -23,11 +28,6 @@ public class TarActionProvider implements BrowserActionProvider {
|
||||
|
||||
private final boolean gz;
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
var sc = model.getFileSystem().getShell().orElseThrow();
|
||||
@@ -59,10 +59,10 @@ public class TarActionProvider implements BrowserActionProvider {
|
||||
}
|
||||
model.refreshSync();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "tar";
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-10
@@ -12,6 +12,11 @@ import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
public class UntarActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "untar";
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
@@ -19,11 +24,6 @@ public class UntarActionProvider implements BrowserActionProvider {
|
||||
private final boolean gz;
|
||||
private final boolean toDirectory;
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
ShellControl sc = model.getFileSystem().getShell().orElseThrow();
|
||||
@@ -45,6 +45,11 @@ public class UntarActionProvider implements BrowserActionProvider {
|
||||
model.refreshSync();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private FilePath getTarget(FilePath name) {
|
||||
return FilePath.of(name.toString()
|
||||
.replaceAll("\\.tar$", "")
|
||||
@@ -52,9 +57,4 @@ public class UntarActionProvider implements BrowserActionProvider {
|
||||
.replaceAll("\\.tgz$", ""));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "untar";
|
||||
}
|
||||
}
|
||||
|
||||
+10
-10
@@ -19,17 +19,17 @@ public class UnzipActionProvider implements BrowserActionProvider {
|
||||
return FilePath.of(name.toString().replaceAll("\\.zip$", ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "unzip";
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
|
||||
private final boolean toDirectory;
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
var sc = model.getFileSystem().getShell().orElseThrow();
|
||||
@@ -64,6 +64,11 @@ public class UnzipActionProvider implements BrowserActionProvider {
|
||||
model.refreshSync();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void runPowershellCommand(ShellControl sc, BrowserFileSystemTabModel model, BrowserEntry entry)
|
||||
throws Exception {
|
||||
var command = CommandBuilder.of().add("Expand-Archive", "-Force");
|
||||
@@ -77,9 +82,4 @@ public class UnzipActionProvider implements BrowserActionProvider {
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "unzip";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@ import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
public class ZipActionProvider implements BrowserActionProvider {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "zip";
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
@SuperBuilder
|
||||
public static class Action extends BrowserAction {
|
||||
@@ -24,11 +29,6 @@ public class ZipActionProvider implements BrowserActionProvider {
|
||||
|
||||
private final boolean directoryContentOnly;
|
||||
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executeImpl() throws Exception {
|
||||
try {
|
||||
@@ -91,10 +91,10 @@ public class ZipActionProvider implements BrowserActionProvider {
|
||||
model.refreshSync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "zip";
|
||||
@Override
|
||||
public boolean isMutation() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import io.xpipe.app.util.BindingsHelper;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ReadOnlyBooleanWrapper;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
@@ -23,7 +24,6 @@ import atlantafx.base.controls.Spacer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public abstract class Comp<S extends CompStructure<?>> {
|
||||
@@ -71,12 +71,6 @@ public abstract class Comp<S extends CompStructure<?>> {
|
||||
return of(() -> new Separator(Orientation.VERTICAL));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <IR extends Region, SIN extends CompStructure<IR>, OR extends Region> Comp<CompStructure<OR>> derive(
|
||||
Comp<SIN> comp, Function<IR, OR> r) {
|
||||
return of(() -> r.apply((IR) comp.createRegion()));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Comp<S>> T apply(Augment<S> augment) {
|
||||
if (augments == null) {
|
||||
@@ -162,6 +156,10 @@ public abstract class Comp<S extends CompStructure<?>> {
|
||||
});
|
||||
}
|
||||
|
||||
public Comp<S> disable(boolean o) {
|
||||
return disable(new ReadOnlyBooleanWrapper(o));
|
||||
}
|
||||
|
||||
public Comp<S> disable(ObservableValue<Boolean> o) {
|
||||
return apply(struc -> {
|
||||
var region = struc.get();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user