diff --git a/app/build.gradle b/app/build.gradle index 246496230..d3da32f6f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/io/xpipe/app/action/AbstractAction.java b/app/src/main/java/io/xpipe/app/action/AbstractAction.java index 4d1501530..0b0af9704 100644 --- a/app/src/main/java/io/xpipe/app/action/AbstractAction.java +++ b/app/src/main/java/io/xpipe/app/action/AbstractAction.java @@ -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 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 toDisplayMap(); diff --git a/app/src/main/java/io/xpipe/app/action/ActionJacksonMapper.java b/app/src/main/java/io/xpipe/app/action/ActionJacksonMapper.java index 03c0e0205..6508a5dbb 100644 --- a/app/src/main/java/io/xpipe/app/action/ActionJacksonMapper.java +++ b/app/src/main/java/io/xpipe/app/action/ActionJacksonMapper.java @@ -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; } } diff --git a/app/src/main/java/io/xpipe/app/action/ActionProvider.java b/app/src/main/java/io/xpipe/app/action/ActionProvider.java index 2ab33b8f5..3abeb1f44 100644 --- a/app/src/main/java/io/xpipe/app/action/ActionProvider.java +++ b/app/src/main/java/io/xpipe/app/action/ActionProvider.java @@ -37,10 +37,6 @@ public interface ActionProvider { default void init() {} - default String getLicensedFeatureId() { - return null; - } - default String getId() { return null; } diff --git a/app/src/main/java/io/xpipe/app/beacon/AppBeaconCache.java b/app/src/main/java/io/xpipe/app/beacon/AppBeaconCache.java index d0889e95f..9452e6c72 100644 --- a/app/src/main/java/io/xpipe/app/beacon/AppBeaconCache.java +++ b/app/src/main/java/io/xpipe/app/beacon/AppBeaconCache.java @@ -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 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); + } } diff --git a/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java b/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java index ad042299b..5bea468a3 100644 --- a/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java +++ b/app/src/main/java/io/xpipe/app/beacon/AppBeaconServer.java @@ -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 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; } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java index 58c4c741f..1dc7afdb7 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/AskpassExchangeImpl.java @@ -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; - } } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonModeExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonModeExchangeImpl.java index e0f63b2a2..3c8d0b17b 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonModeExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonModeExchangeImpl.java @@ -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 diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonOpenExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonOpenExchangeImpl.java index 45b1baed5..787347c29 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonOpenExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonOpenExchangeImpl.java @@ -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; - } } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonVersionExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonVersionExchangeImpl.java index 53b44ec4f..b1c6764bd 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/DaemonVersionExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/DaemonVersionExchangeImpl.java @@ -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() diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/HandshakeExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/HandshakeExchangeImpl.java index d018d5658..0284be1bb 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/HandshakeExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/HandshakeExchangeImpl.java @@ -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; - } } diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/TerminalExternalLaunchExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/TerminalExternalLaunchExchangeImpl.java index 1fd3ccd17..8c2c8b7c4 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/TerminalExternalLaunchExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/TerminalExternalLaunchExchangeImpl.java @@ -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(); - } } diff --git a/app/src/main/java/io/xpipe/app/beacon/mcp/AppMcpServer.java b/app/src/main/java/io/xpipe/app/beacon/mcp/AppMcpServer.java new file mode 100644 index 000000000..cb47634d5 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/mcp/AppMcpServer.java @@ -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(); + 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); + } + } + } + }; + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/mcp/HttpStreamableServerTransportProvider.java b/app/src/main/java/io/xpipe/app/beacon/mcp/HttpStreamableServerTransportProvider.java new file mode 100644 index 000000000..0578c7333 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/mcp/HttpStreamableServerTransportProvider.java @@ -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 sessions = new ConcurrentHashMap<>(); + + private final McpTransportContextExtractor contextExtractor; + private McpStreamableServerSession.Factory sessionFactory; + + private volatile boolean isClosing = false; + + private KeepAliveScheduler keepAliveScheduler; + + HttpStreamableServerTransportProvider( + ObjectMapper objectMapper, + String mcpEndpoint, + boolean disallowDelete, + McpTransportContextExtractor 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 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 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 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 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 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 closeGracefully() { + return Mono.fromRunnable(() -> { + HttpServletStreamableMcpSessionTransport.this.close(); + }); + } + + @Override + public Mono sendMessage(McpSchema.JSONRPCMessage message) { + return sendMessage(message, null); + } + + @Override + public T unmarshalFrom(Object data, TypeReference typeRef) { + return objectMapper.convertValue(data, typeRef); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/mcp/McpSchemaFiles.java b/app/src/main/java/io/xpipe/app/beacon/mcp/McpSchemaFiles.java new file mode 100644 index 000000000..68d528dbb --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/mcp/McpSchemaFiles.java @@ -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); + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/mcp/McpToolHandler.java b/app/src/main/java/io/xpipe/app/beacon/mcp/McpToolHandler.java new file mode 100644 index 000000000..f4d9ff768 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/mcp/McpToolHandler.java @@ -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 { + + 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 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 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 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(); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/beacon/mcp/McpTools.java b/app/src/main/java/io/xpipe/app/beacon/mcp/McpTools.java new file mode 100644 index 000000000..8f4ec096d --- /dev/null +++ b/app/src/main/java/io/xpipe/app/beacon/mcp/McpTools.java @@ -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(); + 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(); + 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(); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserAbstractSessionModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserAbstractSessionModel.java index c11465482..2a6ea1e1c 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserAbstractSessionModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserAbstractSessionModel.java @@ -32,7 +32,8 @@ public class BrowserAbstractSessionModel { } 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) { diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionModel.java index 5e519f629..1c98f04f6 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileChooserSessionModel.java @@ -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 diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionModel.java index 2e63d6eae..b7b850470 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionModel.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFullSessionModel.java @@ -32,6 +32,30 @@ import java.util.*; public class BrowserFullSessionModel extends BrowserAbstractSessionModel { public static final BrowserFullSessionModel DEFAULT = new BrowserFullSessionModel(); + private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this); + private final Property draggingFiles = new SimpleBooleanProperty(); + private final Property globalPinnedTab = new SimpleObjectProperty<>(); + private final ObservableMap splits = FXCollections.observableHashMap(); + private final ObservableValue effectiveRightTab = createEffectiveRightTab(); + private final SequencedSet previousTabs = new LinkedHashSet<>(); + + public BrowserFullSessionModel() { + sessionEntries.addListener((ListChangeListener) 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 draggingFiles = new SimpleBooleanProperty(); - private final Property globalPinnedTab = new SimpleObjectProperty<>(); - private final ObservableMap splits = FXCollections.observableHashMap(); - private final ObservableValue effectiveRightTab = createEffectiveRightTab(); - private final SequencedSet previousTabs = new LinkedHashSet<>(); - private ObservableValue createEffectiveRightTab() { return Bindings.createObjectBinding( () -> { @@ -88,24 +105,6 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel) 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 getAllTabs() { var set = new HashSet(); set.addAll(sessionEntries); @@ -221,8 +220,9 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel { + 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(); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserStoreSessionTab.java b/app/src/main/java/io/xpipe/app/browser/BrowserStoreSessionTab.java index 2b03cdf3c..56ea29e09 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserStoreSessionTab.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserStoreSessionTab.java @@ -23,11 +23,6 @@ public abstract class BrowserStoreSessionTab extends Browse this.name = DataStorage.get().getStoreEntryDisplayName(entry.get()); } - @Override - public ObservableValue getName() { - return new SimpleStringProperty(name); - } - public abstract Comp comp(); public abstract boolean canImmediatelyClose(); @@ -36,6 +31,11 @@ public abstract class BrowserStoreSessionTab extends Browse public abstract void close(); + @Override + public ObservableValue getName() { + return new SimpleStringProperty(name); + } + @Override public String getIcon() { return entry.get().getEffectiveIconFile(); diff --git a/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java b/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java index bcbbef1c8..ed75b4b82 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java +++ b/app/src/main/java/io/xpipe/app/browser/action/BrowserAction.java @@ -62,6 +62,11 @@ public abstract class BrowserAction extends StoreAction { 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 { } } - @Override - protected void afterExecute() { - model.getBusy().set(false); - } - public List getEntries() { if (entries != null) { return entries; diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/ApplyFileEditActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/ApplyFileEditActionProvider.java index 1d7d6801b..f16e9899a 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/ApplyFileEditActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/ApplyFileEditActionProvider.java @@ -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 toDisplayMap() { var map = new LinkedHashMap(); diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/BrowseInNativeManagerActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/BrowseInNativeManagerActionProvider.java index dd2f0961a..684f1b8cd 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/BrowseInNativeManagerActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/BrowseInNativeManagerActionProvider.java @@ -15,6 +15,20 @@ import java.util.List; public class BrowseInNativeManagerActionProvider implements BrowserActionProvider { + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List 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 entries) { - return model.getFileSystem() - .getShell() - .orElseThrow() - .getLocalSystemAccess() - .supportsFileSystemAccess(); - } - - @Override - public String getId() { - return "browseInNativeFileManager"; - } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/ChgrpActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/ChgrpActionProvider.java index ee3010ae9..93a1b580c 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/ChgrpActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/ChgrpActionProvider.java @@ -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; + } } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/ChmodActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/ChmodActionProvider.java index a30035a7a..a7475aef6 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/ChmodActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/ChmodActionProvider.java @@ -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; + } } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/ChownActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/ChownActionProvider.java index d2b5ec48b..0cf7e5128 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/ChownActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/ChownActionProvider.java @@ -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; + } } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/ComputeDirectorySizesActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/ComputeDirectorySizesActionProvider.java index df0be394f..257b5038a 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/ComputeDirectorySizesActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/ComputeDirectorySizesActionProvider.java @@ -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"; - } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/DeleteActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/DeleteActionProvider.java index 9cd36c06e..cd5c3429a 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/DeleteActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/DeleteActionProvider.java @@ -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; + } } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/MoveFileActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/MoveFileActionProvider.java index 796532813..e5c6dbc3b 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/MoveFileActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/MoveFileActionProvider.java @@ -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; + } } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/NewDirectoryActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/NewDirectoryActionProvider.java index e8f7c11e1..f95053bca 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/NewDirectoryActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/NewDirectoryActionProvider.java @@ -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; + } } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/NewFileActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/NewFileActionProvider.java index 524d08931..89b2b5409 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/NewFileActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/NewFileActionProvider.java @@ -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; + } } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/NewLinkActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/NewLinkActionProvider.java index f066fa211..2d60fbe2b 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/NewLinkActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/NewLinkActionProvider.java @@ -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; + } } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenDirectoryActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenDirectoryActionProvider.java index db5d80a61..8be25a34b 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenDirectoryActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenDirectoryActionProvider.java @@ -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()); + } + } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileDefaultActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileDefaultActionProvider.java index 5f5190f5b..7db1195c4 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileDefaultActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileDefaultActionProvider.java @@ -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 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 entries) { - return model.getFileList().getEditing().getValue() == null - && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE); - } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileNativeDetailsActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileNativeDetailsActionProvider.java index 07f187113..73920dfe5 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileNativeDetailsActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileNativeDetailsActionProvider.java @@ -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 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 entries) { - var sc = model.getFileSystem().getShell().orElseThrow(); - return sc.getLocalSystemAccess().supportsFileSystemAccess(); - } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileWithActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileWithActionProvider.java index 10c93efee..8f05e42a8 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileWithActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenFileWithActionProvider.java @@ -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 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 entries) { - return OsType.getLocal().equals(OsType.WINDOWS) - && entries.size() == 1 - && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE); - } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenTerminalActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenTerminalActionProvider.java index 058d19214..34967c1a7 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/OpenTerminalActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/OpenTerminalActionProvider.java @@ -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 entries) { + return entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY); + } + + @Override + public boolean isActive(BrowserFileSystemTabModel model, List 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 entries) { - return entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY); - } - - @Override - public boolean isActive(BrowserFileSystemTabModel model, List entries) { - var t = AppPrefs.get().terminalType().getValue(); - return t != null; - } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBackgroundActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBackgroundActionProvider.java index 5412f054c..43777cd13 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBackgroundActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBackgroundActionProvider.java @@ -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 out = new AtomicReference<>(); AtomicReference 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 toDisplayMap() { + var map = new LinkedHashMap<>(super.toDisplayMap()); + map.remove("Title"); + map.remove("Files"); + map.put("Working Directory", files.getFirst().toString()); + return map; + } } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBrowserActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBrowserActionProvider.java index d61ff28f0..8058d769f 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBrowserActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInBrowserActionProvider.java @@ -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 toDisplayMap() { + var map = new LinkedHashMap<>(super.toDisplayMap()); + map.remove("Files"); + map.put("Working Directory", files.getFirst().toString()); + return map; } } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInTerminalActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInTerminalActionProvider.java index 4797819ec..7e4057424 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInTerminalActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/RunCommandInTerminalActionProvider.java @@ -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 toDisplayMap() { + var map = new LinkedHashMap<>(super.toDisplayMap()); + map.remove("Title"); + map.remove("Files"); + map.put("Working Directory", files.getFirst().toString()); + return map; } } } diff --git a/app/src/main/java/io/xpipe/app/browser/action/impl/TransferFilesActionProvider.java b/app/src/main/java/io/xpipe/app/browser/action/impl/TransferFilesActionProvider.java index df5436a6c..431c32234 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/impl/TransferFilesActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/action/impl/TransferFilesActionProvider.java @@ -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 toDisplayMap() { var name = operation.isMove() ? "Move files" : getDisplayName(); diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserClipboard.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserClipboard.java index 3ec9f913a..a313e38da 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserClipboard.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserClipboard.java @@ -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 currentCopyClipboard = new SimpleObjectProperty<>(); - public static Instance currentDragClipboard; + public static final Property currentCopyClipboard = new GlobalObjectProperty<>(); private static final DataFormat DATA_FORMAT = new DataFormat("application/xpipe-file-list"); + public static Instance currentDragClipboard; static { GlobalClipboard.addListener(new Consumer<>() { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserAlerts.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserDialogs.java similarity index 96% rename from app/src/main/java/io/xpipe/app/browser/file/BrowserAlerts.java rename to app/src/main/java/io/xpipe/app/browser/file/BrowserDialogs.java index 5930eb408..814a7091b 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserAlerts.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserDialogs.java @@ -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(); 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)); diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java index 92d8254be..0d9838dab 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java @@ -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 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(); 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 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 table, TableColumn filenameCol, diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOpener.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOpener.java index a77d5471f..36ccd0d64 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOpener.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOpener.java @@ -190,23 +190,23 @@ public class BrowserFileOpener { return new BrowserFileOutput() { @Override - public void beforeTransfer() {} + public Optional target() { + return Optional.of(model.getEntry().get()); + } @Override public boolean hasOutput() { return true; } - @Override - public Optional 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 target() { + return Optional.of(model.getEntry().get()); + } @Override public boolean hasOutput() { return true; } - @Override - public Optional 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)); diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHelper.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHelper.java index 4fdf8effd..f6a69f945 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHelper.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHelper.java @@ -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 + "\\"; } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHistory.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHistory.java index 6889aa6ce..0d9603e41 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHistory.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemHistory.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java index decd20799..45aaaae79 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java @@ -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 currentPath = new ReadOnlyObjectWrapper<>(); private final BrowserFileSystemHistory history = new BrowserFileSystemHistory(); private final BooleanProperty inOverview = new SimpleBooleanProperty(); - private final Property progress = new SimpleObjectProperty<>(); private final ObservableList terminalRequests = FXCollections.observableArrayList(); private final BooleanProperty transferCancelled = new SimpleBooleanProperty(); - + private final Property progress = new SimpleObjectProperty<>(); + private final ObservableList progressesIntervalHistory = + FXCollections.observableArrayList(); + private final LongProperty progressTransferSpeed = new SimpleLongProperty(); + private final Property progressRemaining = new SimpleObjectProperty<>(); private FileSystem fileSystem; - private BrowserFileSystemSavedState savedState; private BrowserFileSystemCache cache; @@ -72,6 +76,50 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab= 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 getProgress() { + return progress; + } + public Optional 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, 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 { 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 { 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 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 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 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(); @@ -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(); diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserGreetingComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserGreetingComp.java index 448804f97..cae3eeefc 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserGreetingComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserGreetingComp.java @@ -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; } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserHistorySavedStateImpl.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserHistorySavedStateImpl.java index 672370f85..49b01704d 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserHistorySavedStateImpl.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserHistorySavedStateImpl.java @@ -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 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(); diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserLocalFileSystem.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserLocalFileSystem.java index 448fd1886..6b4976f7b 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserLocalFileSystem.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserLocalFileSystem.java @@ -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), diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java index 23053fddf..ed6110eb2 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserNavBarComp.java @@ -34,6 +34,13 @@ import org.kordamp.ikonli.javafx.FontIcon; public class BrowserNavBarComp extends Comp { + 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 { return pathBar; } - public record Structure(HBox box, TextField textField, Button historyButton) implements CompStructure { - - @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 { }); return cm; } + + public record Structure(HBox box, TextField textField, Button historyButton) implements CompStructure { + + @Override + public HBox get() { + return box; + } + } } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java index 686b629b7..911a24dda 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java @@ -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 updateMenuItems(Menu m, BrowserEntry entry, boolean updateInstantly) throws Exception { List 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("..."); diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java index 62b9079de..4d91f49be 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserStatusBarComp.java @@ -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; } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserTerminalDockTabModel.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserTerminalDockTabModel.java index 9ce8fe052..5d4fb4d8d 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserTerminalDockTabModel.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserTerminalDockTabModel.java @@ -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); + } + } } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferComp.java index 44adb6363..66658cfb1 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferComp.java @@ -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())); diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferModel.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferModel.java index b691e86f2..287d141c9 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferModel.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferModel.java @@ -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; } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferProgress.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferProgress.java index 14004a674..8fb8b20dd 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferProgress.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserTransferProgress.java @@ -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 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); - } } diff --git a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconDirectoryType.java b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconDirectoryType.java index 990ece51c..28cdcac30 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconDirectoryType.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconDirectoryType.java @@ -20,13 +20,6 @@ public abstract class BrowserIconDirectoryType { private static final List 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; diff --git a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java index 6ba92c753..9ed544783 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconVariant.java b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconVariant.java index 4df99cc60..50bd7cf5c 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconVariant.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconVariant.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuBranchProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuBranchProvider.java index e556c7ed4..757cffda0 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuBranchProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuBranchProvider.java @@ -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; } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuLeafProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuLeafProvider.java index 03f78eeee..3dd4b4162 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuLeafProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/BrowserMenuLeafProvider.java @@ -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 entries) { - if (getDelegateActionProvider() != null) { - var provider = BrowserActionProviders.forClass(getDelegateActionProvider()); - return provider.isApplicable(model, entries); - } else { - return true; - } - } - @SneakyThrows default AbstractAction createAction(BrowserFileSystemTabModel model, List 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 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 entries) { + if (getDelegateActionProvider() != null) { + var provider = BrowserActionProviders.forClass(getDelegateActionProvider()); + return provider.isApplicable(model, entries); + } else { + return true; + } + } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/MultiExecuteMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/MultiExecuteMenuProvider.java index d6d039a94..f567ff0eb 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/MultiExecuteMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/MultiExecuteMenuProvider.java @@ -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 createCommand(BrowserFileSystemTabModel model, List entries); @Override public List getBranchingActions( @@ -25,25 +26,24 @@ public abstract class MultiExecuteMenuProvider implements BrowserMenuBranchProvi new BrowserMenuLeafProvider() { @Override + @SneakyThrows public void execute(BrowserFileSystemTabModel model, List 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 entries) { + return AppPrefs.get().terminalType().getValue() != null; + } + @Override public ObservableValue getName( BrowserFileSystemTabModel model, List entries) { @@ -52,29 +52,21 @@ public abstract class MultiExecuteMenuProvider implements BrowserMenuBranchProvi "executeInTerminal", t != null ? t.toTranslatedString().getValue() : "?"); } - - @Override - public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { - return AppPrefs.get().terminalType().getValue() != null; - } }, new BrowserMenuLeafProvider() { @Override + @SneakyThrows public void execute(BrowserFileSystemTabModel model, List 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 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 diff --git a/app/src/main/java/io/xpipe/app/browser/menu/MultiExecuteSelectionMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/MultiExecuteSelectionMenuProvider.java deleted file mode 100644 index ad2008e01..000000000 --- a/app/src/main/java/io/xpipe/app/browser/menu/MultiExecuteSelectionMenuProvider.java +++ /dev/null @@ -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 getBranchingActions( - BrowserFileSystemTabModel model, List entries) { - return List.of( - new BrowserMenuLeafProvider() { - - @Override - public void execute(BrowserFileSystemTabModel model, List entries) { - var builder = RunCommandInTerminalActionProvider.Action.builder(); - builder.initEntries(model, entries); - builder.title(getTerminalTitle()); - builder.command(createCommand(model)); - builder.build().executeAsync(); - } - - @Override - public ObservableValue getName( - BrowserFileSystemTabModel model, List entries) { - var t = AppPrefs.get().terminalType().getValue(); - return AppI18n.observable( - "executeInTerminal", - t != null ? t.toTranslatedString().getValue() : "?"); - } - - @Override - public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { - return AppPrefs.get().terminalType().getValue() != null; - } - }, - new BrowserMenuLeafProvider() { - - @Override - public void execute(BrowserFileSystemTabModel model, List entries) { - var builder = RunCommandInBrowserActionProvider.Action.builder(); - builder.initEntries(model, entries); - builder.command(createCommand(model)); - builder.build().executeAsync(); - } - - @Override - public ObservableValue getName( - BrowserFileSystemTabModel model, List entries) { - return AppI18n.observable("runInFileBrowser"); - } - }, - new BrowserMenuLeafProvider() { - - @Override - public void execute(BrowserFileSystemTabModel model, List entries) { - var builder = RunCommandInBackgroundActionProvider.Action.builder(); - builder.initEntries(model, entries); - builder.command(createCommand(model)); - builder.build().executeAsync(); - } - - @Override - public ObservableValue getName( - BrowserFileSystemTabModel model, List entries) { - return AppI18n.observable("runSilent"); - } - }); - } -} diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/BackMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/BackMenuProvider.java index d21b5a690..ee3575794 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/BackMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/BackMenuProvider.java @@ -23,13 +23,18 @@ public class BackMenuProvider implements BrowserMenuLeafProvider { }); } + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + return false; + } + public String getId() { return "back"; } @Override public LabelGraphic getIcon(BrowserFileSystemTabModel model, List 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 entries) { - return false; - } - @Override public boolean isActive(BrowserFileSystemTabModel model, List entries) { return model.getHistory().canGoBackProperty().get(); diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/BrowseInNativeManagerMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/BrowseInNativeManagerMenuProvider.java index d0dfee5b0..da5af2645 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/BrowseInNativeManagerMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/BrowseInNativeManagerMenuProvider.java @@ -26,16 +26,16 @@ public class BrowseInNativeManagerMenuProvider implements BrowserMenuLeafProvide } @Override - public boolean acceptsEmptySelection() { - return true; + public ObservableValue getName(BrowserFileSystemTabModel model, List 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 getName(BrowserFileSystemTabModel model, List 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; } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/ChgrpMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/ChgrpMenuProvider.java index b8cd60bc4..a80de6791 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/ChgrpMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/ChgrpMenuProvider.java @@ -23,6 +23,20 @@ import java.util.stream.Stream; public class ChgrpMenuProvider implements BrowserMenuBranchProvider { + private static List getLeafActions(BrowserFileSystemTabModel model, boolean recursive) { + List actions = Stream.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 entries) { return new LabelGraphic.IconGraphic("mdi2a-account-group-outline"); @@ -93,20 +107,6 @@ public class ChgrpMenuProvider implements BrowserMenuBranchProvider { } } - private static List getLeafActions(BrowserFileSystemTabModel model, boolean recursive) { - List actions = Stream.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; diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/ChmodMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/ChmodMenuProvider.java index 7f66e888b..3b08f98c1 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/ChmodMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/ChmodMenuProvider.java @@ -22,6 +22,20 @@ import java.util.List; public class ChmodMenuProvider implements BrowserMenuBranchProvider { + private static List 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 entries) { return new LabelGraphic.IconGraphic("mdi2w-wrench-outline"); @@ -91,20 +105,6 @@ public class ChmodMenuProvider implements BrowserMenuBranchProvider { } } - private static List 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; diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/ChownMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/ChownMenuProvider.java index f2f8e97d3..20d08941d 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/ChownMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/ChownMenuProvider.java @@ -23,6 +23,19 @@ import java.util.stream.Stream; public class ChownMenuProvider implements BrowserMenuBranchProvider { + private static List getLeafActions(BrowserFileSystemTabModel model, boolean recursive) { + var actions = Stream.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 entries) { return new LabelGraphic.IconGraphic("mdi2a-account-edit"); @@ -93,19 +106,6 @@ public class ChownMenuProvider implements BrowserMenuBranchProvider { } } - private static List getLeafActions(BrowserFileSystemTabModel model, boolean recursive) { - var actions = Stream.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; diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/ComputeDirectorySizesMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/ComputeDirectorySizesMenuProvider.java index e86a1fb7e..a9bbbe712 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/ComputeDirectorySizesMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/ComputeDirectorySizesMenuProvider.java @@ -16,13 +16,6 @@ import java.util.List; public class ComputeDirectorySizesMenuProvider implements BrowserMenuLeafProvider { - @Override - public AbstractAction createAction(BrowserFileSystemTabModel model, List 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 getName(BrowserFileSystemTabModel model, List 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 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 entries) { + var builder = ComputeDirectorySizesActionProvider.Action.builder(); + builder.initEntries(model, entries); + return builder.build(); } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/CopyMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/CopyMenuProvider.java index 4adce0fa0..050c5639c 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/CopyMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/CopyMenuProvider.java @@ -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 getName(BrowserFileSystemTabModel model, List entries) { return AppI18n.observable("copy"); } + + @Override + public boolean acceptsEmptySelection() { + return true; + } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/CopyPathMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/CopyPathMenuProvider.java index d7e7db8b4..784280eb9 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/CopyPathMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/CopyPathMenuProvider.java @@ -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 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 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 getName( BrowserFileSystemTabModel model, List entries) { @@ -90,18 +108,6 @@ public class CopyPathMenuProvider implements BrowserMenuBranchProvider { return AppI18n.observable("absoluteLinkPaths"); } - @Override - public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { - return entries.stream() - .allMatch(browserEntry -> - browserEntry.getRawFileEntry().getKind() == FileKind.LINK); - } - - @Override - public boolean automaticallyResolveLinks() { - return false; - } - @Override public void execute(BrowserFileSystemTabModel model, List 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 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 entries) { - return entries.stream().anyMatch(entry -> entry.getRawFileEntry() - .getPath() - .toString() - .contains(" ")); - } - @Override public void execute(BrowserFileSystemTabModel model, List 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 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 getName( BrowserFileSystemTabModel model, List entries) { @@ -190,6 +207,14 @@ public class CopyPathMenuProvider implements BrowserMenuBranchProvider { return AppI18n.observable("linkFileNames"); } + @Override + public void execute(BrowserFileSystemTabModel model, List 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 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 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 entries) { - return entries.stream().anyMatch(entry -> entry.getRawFileEntry() - .getPath() - .getFileName() - .contains(" ")); - } - @Override public void execute(BrowserFileSystemTabModel model, List 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 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); - } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/DeleteMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/DeleteMenuProvider.java index adf9e51b8..e3834ec94 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/DeleteMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/DeleteMenuProvider.java @@ -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 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 getName(BrowserFileSystemTabModel model, List entries) { return AppI18n.observable( diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/DownloadMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/DownloadMenuProvider.java index 0520bf92b..e791f1024 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/DownloadMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/DownloadMenuProvider.java @@ -27,6 +27,15 @@ public class DownloadMenuProvider implements BrowserMenuLeafProvider { fullSessionModel.getLocalTransfersStage().drop(model, entries); } + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List 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 getName(BrowserFileSystemTabModel model, List entries) { return AppI18n.observable("download"); } - - @Override - public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { - var transfer = model.getBrowserModel(); - if (!(transfer instanceof BrowserFullSessionModel)) { - return false; - } - return true; - } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/EditFileMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/EditFileMenuProvider.java index a9e6910d4..c2fd07744 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/EditFileMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/EditFileMenuProvider.java @@ -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 entries) { ThreadHelper.runAsync(() -> { @@ -34,6 +29,11 @@ public class EditFileMenuProvider implements BrowserMenuLeafProvider { }); } + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + return entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE); + } + @Override public LabelGraphic getIcon(BrowserFileSystemTabModel model, List 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 getName(BrowserFileSystemTabModel model, List 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 entries) { - return entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE); - } - @Override public boolean isActive(BrowserFileSystemTabModel model, List entries) { var e = AppPrefs.get().externalEditor().getValue(); diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/FollowLinkMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/FollowLinkMenuProvider.java index 250ac1e7b..9c94170c5 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/FollowLinkMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/FollowLinkMenuProvider.java @@ -20,6 +20,19 @@ public class FollowLinkMenuProvider implements BrowserMenuLeafProvider { model.cdAsync(target); } + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + return entries.size() == 1 + && entries.stream() + .allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.LINK + && entry.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY); + } + + @Override + public boolean automaticallyResolveLinks() { + return false; + } + @Override public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { return new LabelGraphic.IconGraphic("mdi2a-arrow-top-right-thick"); @@ -34,17 +47,4 @@ public class FollowLinkMenuProvider implements BrowserMenuLeafProvider { public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { return AppI18n.observable("followLink"); } - - @Override - public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { - return entries.size() == 1 - && entries.stream() - .allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.LINK - && entry.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY); - } - - @Override - public boolean automaticallyResolveLinks() { - return false; - } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/ForwardMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/ForwardMenuProvider.java index c7e485204..9195a784c 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/ForwardMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/ForwardMenuProvider.java @@ -23,13 +23,18 @@ public class ForwardMenuProvider implements BrowserMenuLeafProvider { }); } + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + return false; + } + public String getId() { return "forward"; } @Override public LabelGraphic getIcon(BrowserFileSystemTabModel model, List 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 entries) { - return false; - } - @Override public boolean isActive(BrowserFileSystemTabModel model, List entries) { return model.getHistory().canGoForthProperty().get(); diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/JarMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/JarMenuProvider.java index 842ab33b8..6d63e656c 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/JarMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/JarMenuProvider.java @@ -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 createCommand(BrowserFileSystemTabModel model, List entries) { + return entries.stream() + .map(browserEntry -> { + return CommandBuilder.of() + .add("java", "-jar") + .addFile(browserEntry.getRawFileEntry().getPath()); + }) + .toList(); + } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/NewItemMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/NewItemMenuProvider.java index 1c3f9bb25..77465c75a 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/NewItemMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/NewItemMenuProvider.java @@ -37,13 +37,13 @@ public class NewItemMenuProvider implements BrowserMenuBranchProvider { } @Override - public boolean acceptsEmptySelection() { - return true; + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return AppI18n.observable("new"); } @Override - public ObservableValue getName(BrowserFileSystemTabModel model, List 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 entries) { + return model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS; + } + @Override public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { return new LabelGraphic.CompGraphic(BrowserIcons.createDefaultFileIcon()); @@ -196,11 +201,6 @@ public class NewItemMenuProvider implements BrowserMenuBranchProvider { BrowserFileSystemTabModel model, List entries) { return AppI18n.observable("symbolicLink"); } - - @Override - public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { - return model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS; - } }); } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenDirectoryInNewTabMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenDirectoryInNewTabMenuProvider.java index 11b1e8183..7a44200f9 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenDirectoryInNewTabMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenDirectoryInNewTabMenuProvider.java @@ -26,6 +26,13 @@ public class OpenDirectoryInNewTabMenuProvider implements BrowserMenuLeafProvide } } + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List 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 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 getName(BrowserFileSystemTabModel model, List entries) { return AppI18n.observable("openInNewTab"); } @Override - public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { - return model.getBrowserModel() instanceof BrowserFullSessionModel - && entries.size() == 1 - && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY); + public boolean acceptsEmptySelection() { + return true; } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileWithMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileWithMenuProvider.java index 2dd143d49..dc2bba783 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileWithMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenFileWithMenuProvider.java @@ -25,6 +25,13 @@ public class OpenFileWithMenuProvider implements BrowserMenuLeafProvider { return OpenFileWithActionProvider.class; } + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List 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 entries) { return new LabelGraphic.IconGraphic("mdi2b-book-open-page-variant-outline"); @@ -44,11 +51,4 @@ public class OpenFileWithMenuProvider implements BrowserMenuLeafProvider { public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { return AppI18n.observable("openFileWith"); } - - @Override - public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { - return OsType.getLocal().equals(OsType.WINDOWS) - && entries.size() == 1 - && entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE); - } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenNativeFileDetailsMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenNativeFileDetailsMenuProvider.java index a02575c63..d298b1530 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenNativeFileDetailsMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenNativeFileDetailsMenuProvider.java @@ -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 getName(BrowserFileSystemTabModel model, List entries) { return AppI18n.observable("showDetails"); } + + @Override + public boolean acceptsEmptySelection() { + return true; + } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenTerminalInDirectoryMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenTerminalInDirectoryMenuProvider.java index 55d94e167..fbdbcbf90 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenTerminalInDirectoryMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/OpenTerminalInDirectoryMenuProvider.java @@ -22,11 +22,6 @@ import java.util.List; public class OpenTerminalInDirectoryMenuProvider implements BrowserMenuLeafProvider { - @Override - public Class getDelegateActionProvider() { - return OpenTerminalActionProvider.class; - } - @Override public void execute(BrowserFileSystemTabModel model, List entries) { var dirs = entries.size() > 0 @@ -42,6 +37,16 @@ public class OpenTerminalInDirectoryMenuProvider implements BrowserMenuLeafProvi } } + @Override + public Class getDelegateActionProvider() { + return OpenTerminalActionProvider.class; + } + + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List 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 entries) { - return entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY); - } - @Override public boolean isActive(BrowserFileSystemTabModel model, List entries) { var t = AppPrefs.get().terminalType().getValue(); diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/PasteMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/PasteMenuProvider.java index e8894b3ed..854551bff 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/PasteMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/PasteMenuProvider.java @@ -42,6 +42,14 @@ public class PasteMenuProvider implements BrowserMenuLeafProvider { BrowserFileTransferMode.COPY); } + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + return (entries.size() == 1 + && entries.stream() + .allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY)) + || entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE); + } + @Override public LabelGraphic getIcon(BrowserFileSystemTabModel model, List 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 getName(BrowserFileSystemTabModel model, List entries) { return AppI18n.observable("paste"); } @Override - public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { - return (entries.size() == 1 - && entries.stream() - .allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.DIRECTORY)) - || entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE); + public boolean acceptsEmptySelection() { + return true; } @Override diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/RefreshDirectoryMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/RefreshDirectoryMenuProvider.java index b31a153ee..0fb176217 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/RefreshDirectoryMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/RefreshDirectoryMenuProvider.java @@ -23,6 +23,11 @@ public class RefreshDirectoryMenuProvider implements BrowserMenuLeafProvider { }); } + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List 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 entries) { - return false; - } - @Override public boolean isActive(BrowserFileSystemTabModel model, List entries) { return !model.getInOverview().get(); diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/RenameMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/RenameMenuProvider.java index c7a4ae1ca..4b1b43964 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/RenameMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/RenameMenuProvider.java @@ -22,6 +22,16 @@ public class RenameMenuProvider implements BrowserMenuLeafProvider { model.getFileList().getEditing().setValue(entries.getFirst()); } + @Override + public boolean isApplicable(BrowserFileSystemTabModel model, List entries) { + return entries.size() == 1 && entries.getFirst().getRawFileEntry().getKind() != FileKind.LINK; + } + + @Override + public boolean automaticallyResolveLinks() { + return false; + } + @Override public LabelGraphic getIcon(BrowserFileSystemTabModel model, List 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 entries) { - return entries.size() == 1 && entries.getFirst().getRawFileEntry().getKind() != FileKind.LINK; - } - - @Override - public boolean automaticallyResolveLinks() { - return false; - } - @Override public String getId() { return "renameFile"; diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/RunFileMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/RunFileMenuProvider.java index abea9585a..98efc84a3 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/RunFileMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/RunFileMenuProvider.java @@ -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 createCommand(BrowserFileSystemTabModel model, List 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(); } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUntarMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUntarMenuProvider.java index d73ada71d..7e77d08b5 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUntarMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUntarMenuProvider.java @@ -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 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 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 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$", "")); + } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipUnixMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipUnixMenuProvider.java index 2892479eb..31e01bdb4 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipUnixMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipUnixMenuProvider.java @@ -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 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 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 entries) { + var builder = UnzipActionProvider.Action.builder(); + builder.initEntries(model, entries); + builder.toDirectory(toDirectory); + return builder.build(); } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipWindowsActionProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipWindowsActionProvider.java index 5d353c825..dd25c2aad 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipWindowsActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/BaseUnzipWindowsActionProvider.java @@ -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 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 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 entries) { - return entries.stream() - .allMatch(entry -> - entry.getRawFileEntry().getPath().toString().endsWith(".zip")) - && model.getFileSystem().getShell().orElseThrow().getOsType().equals(OsType.WINDOWS); - } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/CompressMenuProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/CompressMenuProvider.java index 19eef8a0e..615f7863c 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/CompressMenuProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/CompressMenuProvider.java @@ -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 getName(BrowserFileSystemTabModel model, List entries) { - return AppI18n.observable(directory ? "excludeRoot" : "includeRoot"); - } - - @Override - public LabelGraphic getIcon(BrowserFileSystemTabModel model, List entries) { - return directory - ? new LabelGraphic.IconGraphic("mdi2f-file-tree") - : new LabelGraphic.IconGraphic("mdi2f-file-outline"); - } - - @Override - public List getBranchingActions( - BrowserFileSystemTabModel model, List 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 entries) { + return directory + ? new LabelGraphic.IconGraphic("mdi2f-file-tree") + : new LabelGraphic.IconGraphic("mdi2f-file-outline"); + } + + @Override + public ObservableValue getName(BrowserFileSystemTabModel model, List entries) { + return AppI18n.observable(directory ? "excludeRoot" : "includeRoot"); + } + + @Override + public List getBranchingActions( + BrowserFileSystemTabModel model, List 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) { diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/TarActionProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/TarActionProvider.java index 391aa18b0..14e77a555 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/TarActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/TarActionProvider.java @@ -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; + } } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarActionProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarActionProvider.java index abc80a630..4fd7dff67 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UntarActionProvider.java @@ -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"; - } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipActionProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipActionProvider.java index 05f33cd04..33568ab23 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/UnzipActionProvider.java @@ -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"; - } } diff --git a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/ZipActionProvider.java b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/ZipActionProvider.java index 89781e17d..bbecc3593 100644 --- a/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/ZipActionProvider.java +++ b/app/src/main/java/io/xpipe/app/browser/menu/impl/compress/ZipActionProvider.java @@ -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; + } } } diff --git a/app/src/main/java/io/xpipe/app/comp/Comp.java b/app/src/main/java/io/xpipe/app/comp/Comp.java index 5870c63cd..89d35967d 100644 --- a/app/src/main/java/io/xpipe/app/comp/Comp.java +++ b/app/src/main/java/io/xpipe/app/comp/Comp.java @@ -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> { @@ -71,12 +71,6 @@ public abstract class Comp> { return of(() -> new Separator(Orientation.VERTICAL)); } - @SuppressWarnings("unchecked") - public static , OR extends Region> Comp> derive( - Comp comp, Function r) { - return of(() -> r.apply((IR) comp.createRegion())); - } - @SuppressWarnings("unchecked") public > T apply(Augment augment) { if (augments == null) { @@ -162,6 +156,10 @@ public abstract class Comp> { }); } + public Comp disable(boolean o) { + return disable(new ReadOnlyBooleanWrapper(o)); + } + public Comp disable(ObservableValue o) { return apply(struc -> { var region = struc.get(); diff --git a/app/src/main/java/io/xpipe/app/comp/base/AppMainWindowContentComp.java b/app/src/main/java/io/xpipe/app/comp/base/AppMainWindowContentComp.java index 1eba7126e..7b9c8893d 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/AppMainWindowContentComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/AppMainWindowContentComp.java @@ -74,7 +74,7 @@ public class AppMainWindowContentComp extends SimpleComp { // This allows for assigning logos even if AppImages has not been initialized yet var dir = "img/logo/"; - AppResources.with(AppResources.XPIPE_MODULE, dir, path -> { + AppResources.with(AppResources.MAIN_MODULE, dir, path -> { var image = AppPrefs.get() != null && AppPrefs.get().theme().getValue().isDark() ? path.resolve("loading-160-dark.png") @@ -82,8 +82,8 @@ public class AppMainWindowContentComp extends SimpleComp { loadingIcon.setImage(AppImages.loadImage(image)); }); - var version = new LabelComp((AppProperties.get().isStaging() ? "XPipe PTB" : "XPipe") + " " - + AppProperties.get().getVersion()); + var version = new LabelComp( + (AppNames.ofCurrent().getName()) + " " + AppProperties.get().getVersion()); version.apply(struc -> { AppFontSizes.apply(struc.get(), appFontSizes -> "15"); struc.get().setOpacity(0.65); diff --git a/app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java b/app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java index b7e30cd5b..1f0047a59 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java @@ -6,6 +6,7 @@ import io.xpipe.app.comp.SimpleCompStructure; import io.xpipe.app.util.LabelGraphic; import io.xpipe.app.util.PlatformThread; +import javafx.beans.property.ReadOnlyIntegerWrapper; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; import javafx.css.Size; @@ -55,14 +56,16 @@ public class ButtonComp extends Comp> { var n = t.createGraphicNode(); button.setGraphic(n); if (n instanceof FontIcon f && button.getFont() != null) { - f.setIconSize((int) new Size(button.getFont().getSize(), SizeUnits.PT).pixels()); + f.iconSizeProperty().bind(new ReadOnlyIntegerWrapper((int) + new Size(button.getFont().getSize(), SizeUnits.PT).pixels())); } }); }); button.fontProperty().subscribe(c -> { if (button.getGraphic() instanceof FontIcon f) { - f.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels()); + f.iconSizeProperty() + .bind(new ReadOnlyIntegerWrapper((int) new Size(c.getSize(), SizeUnits.PT).pixels())); } }); } diff --git a/app/src/main/java/io/xpipe/app/comp/base/ChoiceComp.java b/app/src/main/java/io/xpipe/app/comp/base/ChoiceComp.java index 4983c22f7..58d17b54a 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ChoiceComp.java @@ -83,6 +83,7 @@ public class ChoiceComp extends Comp>> { }); cb.getStyleClass().add("choice-comp"); + cb.setMaxWidth(10000); return new SimpleCompStructure<>(cb); } } diff --git a/app/src/main/java/io/xpipe/app/comp/base/ComboTextFieldComp.java b/app/src/main/java/io/xpipe/app/comp/base/ComboTextFieldComp.java index 20dba1ef7..d62ab4080 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ComboTextFieldComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ComboTextFieldComp.java @@ -4,12 +4,11 @@ import io.xpipe.app.comp.Comp; import io.xpipe.app.comp.CompStructure; import io.xpipe.app.comp.SimpleCompStructure; import io.xpipe.app.util.PlatformThread; -import io.xpipe.core.FilePath; import javafx.application.Platform; import javafx.beans.property.Property; import javafx.beans.value.ObservableValue; -import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.scene.control.ComboBox; import javafx.scene.control.ListCell; import javafx.scene.input.KeyCode; @@ -17,21 +16,22 @@ import javafx.scene.input.KeyEvent; import lombok.Setter; -import java.util.List; import java.util.Objects; import java.util.function.Supplier; public class ComboTextFieldComp extends Comp>> { private final Property value; - private final List predefinedValues; + private final ObservableList predefinedValues; private final Supplier> customCellFactory; @Setter private ObservableValue prompt; public ComboTextFieldComp( - Property value, List predefinedValues, Supplier> customCellFactory) { + Property value, + ObservableList predefinedValues, + Supplier> customCellFactory) { this.value = value; this.predefinedValues = predefinedValues; this.customCellFactory = customCellFactory; @@ -39,7 +39,7 @@ public class ComboTextFieldComp extends Comp>> { @Override public CompStructure> createBase() { - var text = new ComboBox<>(FXCollections.observableList(predefinedValues)); + var text = new ComboBox<>(predefinedValues); text.addEventFilter(KeyEvent.ANY, event -> { Platform.runLater(() -> { text.commitValue(); diff --git a/app/src/main/java/io/xpipe/app/comp/base/ContextualFileReferenceChoiceComp.java b/app/src/main/java/io/xpipe/app/comp/base/ContextualFileReferenceChoiceComp.java index 13bd74571..b817ce52d 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ContextualFileReferenceChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ContextualFileReferenceChoiceComp.java @@ -22,6 +22,7 @@ import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; import javafx.scene.control.ListCell; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; @@ -38,13 +39,6 @@ import java.util.List; public class ContextualFileReferenceChoiceComp extends Comp> { - @Value - public static class PreviousFileReference { - - String displayName; - Path path; - } - private final Property> fileSystem; private final Property filePath; private final ContextualFileReferenceSync sync; @@ -185,7 +179,7 @@ public class ContextualFileReferenceChoiceComp extends Comp> prop.addListener((observable, oldValue, newValue) -> { filePath.setValue(newValue != null ? FilePath.of(newValue) : null); }); - var combo = new ComboTextFieldComp(prop, items, () -> { + var combo = new ComboTextFieldComp(prop, FXCollections.observableList(items), () -> { return new ListCell<>() { @Override protected void updateItem(String item, boolean empty) { @@ -203,9 +197,11 @@ public class ContextualFileReferenceChoiceComp extends Comp> } }; }); - combo.setPrompt(Bindings.createStringBinding(() -> { - return filePath.getValue() != null ? filePath.getValue().toString() : null; - }, filePath)); + combo.setPrompt(Bindings.createStringBinding( + () -> { + return filePath.getValue() != null ? filePath.getValue().toString() : null; + }, + filePath)); combo.hgrow(); combo.styleClass(Styles.LEFT_PILL); combo.grow(false, true); @@ -237,4 +233,15 @@ public class ContextualFileReferenceChoiceComp extends Comp> return fileNameComp; } + + @Value + public static class PreviousFileReference { + + String displayName; + Path path; + + public static PreviousFileReference of(Path file) { + return new PreviousFileReference(file.toString(), file); + } + } } diff --git a/app/src/main/java/io/xpipe/app/comp/base/DropdownComp.java b/app/src/main/java/io/xpipe/app/comp/base/DropdownComp.java index 524c5ac0e..39c97342b 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/DropdownComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/DropdownComp.java @@ -7,6 +7,7 @@ import io.xpipe.app.comp.augment.ContextMenuAugment; import io.xpipe.app.util.ContextMenuHelper; import javafx.beans.binding.Bindings; +import javafx.beans.property.ReadOnlyIntegerWrapper; import javafx.beans.value.ObservableValue; import javafx.css.Size; import javafx.css.SizeUnits; @@ -53,7 +54,8 @@ public class DropdownComp extends Comp> { var graphic = new FontIcon("mdi2c-chevron-double-down"); button.fontProperty().subscribe(c -> { - graphic.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels()); + graphic.iconSizeProperty() + .bind(new ReadOnlyIntegerWrapper((int) new Size(c.getSize(), SizeUnits.PT).pixels())); }); button.setGraphic(graphic); diff --git a/app/src/main/java/io/xpipe/app/comp/base/FontIconComp.java b/app/src/main/java/io/xpipe/app/comp/base/FontIconComp.java index 685c2a776..16a2d26fe 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/FontIconComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/FontIconComp.java @@ -4,7 +4,6 @@ import io.xpipe.app.comp.Comp; import io.xpipe.app.comp.CompStructure; import io.xpipe.app.util.PlatformThread; -import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; import javafx.scene.layout.StackPane; @@ -17,10 +16,6 @@ public class FontIconComp extends Comp { private final ObservableValue icon; - public FontIconComp(String icon) { - this.icon = new SimpleStringProperty(icon); - } - @Override public FontIconComp.Structure createBase() { var fi = new FontIcon(); diff --git a/app/src/main/java/io/xpipe/app/comp/base/IconButtonComp.java b/app/src/main/java/io/xpipe/app/comp/base/IconButtonComp.java index ba94e73e5..38bf38584 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/IconButtonComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/IconButtonComp.java @@ -6,6 +6,7 @@ import io.xpipe.app.comp.SimpleCompStructure; import io.xpipe.app.util.LabelGraphic; import io.xpipe.app.util.PlatformThread; +import javafx.beans.property.ReadOnlyIntegerWrapper; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; import javafx.css.Size; @@ -50,17 +51,21 @@ public class IconButtonComp extends Comp> { public CompStructure