Squash merge branch 18-release into master

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

Some files were not shown because too many files have changed in this diff Show More