Squash merge branch 19-release into master

This commit is contained in:
crschnick
2025-11-07 09:20:38 +00:00
parent e938df61a9
commit 6404bacbe0
728 changed files with 10297 additions and 5842 deletions
+6 -6
View File
@@ -26,16 +26,16 @@ You should therefore always check out the matching version tag for your local re
You can find the available version tags at https://github.com/xpipe-io/xpipe/tags.
So for example if you currently have XPipe `16.0` installed, you should run `git reset --hard 16.0` first to properly compile against it.
You need to have JDK for Java 24 installed to compile the project.
You need to have JDK for Java 25 installed to compile the project.
If you are on Linux or macOS, you can easily accomplish that by running
```bash
curl -s "https://get.sdkman.io" | bash
. "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install java 24-graalce
sdk default java 24-graalce
sdk install java 25-graalce
sdk default java 25-graalce
```
.
On Windows, you have to manually install a JDK, e.g. from [Adoptium](https://adoptium.net/temurin/releases/?version=24).
On Windows, you have to manually install a JDK, e.g. from [Adoptium](https://adoptium.net/temurin/releases/?version=25).
You can configure a few development options in the file `app/dev.properties` which will be automatically generated when gradle is first run.
@@ -51,7 +51,7 @@ You are also able to properly debug the built production application:
## Modularity and IDEs
All XPipe components target [Java 24](https://openjdk.java.net/projects/jdk/24/) and make full use of the Java Module System (JPMS).
All XPipe components target [Java 25](https://openjdk.java.net/projects/jdk/25/) and make full use of the Java Module System (JPMS).
All components are modularized, including all their dependencies.
In case a dependency is (sadly) not modularized yet, module information is manually added using [extra-java-module-info](https://github.com/gradlex-org/extra-java-module-info).
Further, note that as this is a pretty complicated Java project that fully utilizes modularity,
@@ -59,7 +59,7 @@ many IDEs still have problems building this project properly.
For example, you can't build this project in eclipse or vscode as it will complain about missing modules.
The tested and recommended IDE is IntelliJ.
When setting up the project in IntelliJ, make sure that the correct JDK (Java 24)
When setting up the project in IntelliJ, make sure that the correct JDK (Java 25)
is selected both for the project and for gradle itself.
## Contributing guide
+1 -1
View File
@@ -19,7 +19,7 @@ It currently supports:
- [Proxmox PVE](https://docs.xpipe.io/guide/proxmox) virtual machines and containers
- [Hyper-V](https://docs.xpipe.io/guide/hyperv), [KVM](https://docs.xpipe.io/guide/kvm), [VMware Player/Workstation/Fusion](https://docs.xpipe.io/guide/vmware) virtual machines
- [Kubernetes](https://docs.xpipe.io/guide/kubernetes) clusters, pods, and containers
- [Tailscale](https://docs.xpipe.io/guide/tailscale) and [Teleport](https://docs.xpipe.io/guide/teleport) connections
- [Tailscale](https://docs.xpipe.io/guide/tailscale), [Netbird](https://docs.xpipe.io/guide/netbird), and [Teleport](https://docs.xpipe.io/guide/teleport) connections
- Windows Subsystem for Linux, Cygwin, and MSYS2 environments
- [Powershell Remote Sessions](https://docs.xpipe.io/guide/pssession)
- [RDP](https://docs.xpipe.io/guide/rdp) and [VNC](https://docs.xpipe.io/guide/vnc) connections
+20 -12
View File
@@ -48,10 +48,17 @@ dependencies {
api 'com.vladsch.flexmark:flexmark-ext-yaml-front-matter:0.64.8'
api 'com.vladsch.flexmark:flexmark-ext-toc:0.64.8'
api ('io.modelcontextprotocol.sdk:mcp:0.11.2') {
api ('io.modelcontextprotocol.sdk:mcp-core:0.14.1') {
exclude group: "com.ethlo.time", module: "itu"
}
api ('io.modelcontextprotocol.sdk:mcp-json:0.14.1') {
exclude group: "com.ethlo.time", module: "itu"
}
api ('io.modelcontextprotocol.sdk:mcp-json-jackson2:0.13.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.9"
api "org.reactivestreams:reactive-streams:1.0.4"
api ("com.networknt:json-schema-validator:1.5.8") {
@@ -59,22 +66,23 @@ dependencies {
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 "com.github.weisj:jsvg:1.7.2"
api 'io.xpipe:vernacular:1.16'
api 'org.bouncycastle:bcprov-jdk18on:1.81'
api 'info.picocli:picocli:4.7.7'
api 'org.apache.commons:commons-lang3:3.18.0'
api 'io.sentry:sentry:8.13.3'
api 'io.sentry:sentry:8.20.0'
api 'commons-io:commons-io:2.20.0'
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.19.2"
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.19.2"
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 "com.fasterxml.jackson.core:jackson-databind:2.20.0"
api "com.fasterxml.jackson.core:jackson-annotations:2.20"
api "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.20.0"
api "org.kordamp.ikonli:ikonli-material2-pack:12.4.0"
api "org.kordamp.ikonli:ikonli-materialdesign2-pack: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'
api "org.kordamp.ikonli:ikonli-javafx:12.4.0"
api "org.slf4j:slf4j-api:2.0.17"
api "org.slf4j:slf4j-jdk-platform-logging:2.0.17"
api 'io.xpipe:modulefs:0.1.7'
api 'net.synedra:validatorfx:0.4.2'
api files("$rootDir/gradle/gradle_scripts/atlantafx-base-2.0.2.jar")
}
+6 -4
View File
@@ -1,7 +1,8 @@
package io.xpipe.app;
import io.xpipe.app.core.AppNames;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.core.mode.AppOperationMode;
public class Main {
@@ -17,13 +18,14 @@ public class Main {
if (args.length == 1 && args[0].equals("--help")) {
System.out.println(
"""
The daemon executable xpiped does not accept any command-line arguments.
The daemon executable %s does not accept any command-line arguments.
For a reference on how to use xpipe from the command-line, take a look at https://docs.xpipe.io/cli.
""");
"""
.formatted(AppNames.ofCurrent().getExecutableName()));
return;
}
OperationMode.init(args);
AppOperationMode.init(args);
}
}
@@ -8,8 +8,8 @@ import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.util.DataStoreFormatter;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.app.util.LicensedFeature;
import io.xpipe.app.util.ThreadHelper;
@@ -40,9 +40,11 @@ public abstract class AbstractAction {
AppLayoutModel.get().getQueueEntries().add(queueEntry);
pick = action -> {
cancelPick();
var modal = ModalOverlay.of("actionShortcuts", new ActionPickComp(action).prefWidth(600));
modal.show();
if (action instanceof SerializableAction) {
cancelPick();
var modal = ModalOverlay.of("actionShortcuts", new ActionPickComp(action).prefWidth(600));
modal.show();
}
};
}
@@ -10,8 +10,8 @@ import io.xpipe.app.hub.action.StoreAction;
import io.xpipe.app.hub.comp.StoreChoiceComp;
import io.xpipe.app.hub.comp.StoreListChoiceComp;
import io.xpipe.app.hub.comp.StoreViewState;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.*;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
@@ -69,7 +69,7 @@ public class ActionConfigComp extends SimpleComp {
singleProp.set((DataStoreEntryRef<DataStore>) s);
singleProp.addListener((obs, o, n) -> {
if (action.getValue() instanceof StoreAction<?> sa) {
if (action.getValue() instanceof StoreAction<?> sa && n != null) {
action.setValue(sa.withRef(n.asNeeded()));
}
});
@@ -9,8 +9,8 @@ import io.xpipe.app.hub.action.MultiStoreAction;
import io.xpipe.app.hub.action.StoreAction;
import io.xpipe.app.hub.comp.StoreListChoiceComp;
import io.xpipe.app.hub.comp.StoreViewState;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.OptionsBuilder;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
@@ -2,7 +2,7 @@ package io.xpipe.app.action;
import io.xpipe.app.comp.base.ModalOverlayContentComp;
import io.xpipe.app.comp.base.ScrollComp;
import io.xpipe.app.util.OptionsBuilder;
import io.xpipe.app.platform.OptionsBuilder;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.layout.Region;
@@ -8,9 +8,14 @@ import io.xpipe.app.comp.base.InputGroupComp;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppInstallation;
import io.xpipe.app.platform.BindingsHelper;
import io.xpipe.app.platform.ClipboardHelper;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.update.AppDistributionType;
import io.xpipe.app.util.*;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.layout.Region;
@@ -62,6 +67,7 @@ public class ActionShortcutComp extends SimpleComp {
field.apply(struc -> struc.get().setEditable(false));
var group = new InputGroupComp(List.of(field, copyButton));
group.setHeightReference(copyButton);
group.hide(Bindings.isNull(url));
return group;
}
@@ -76,7 +82,9 @@ public class ActionShortcutComp extends SimpleComp {
});
var copyButton = new ButtonComp(null, new FontIcon("mdi2f-file-move-outline"), () -> {
ThreadHelper.runFailableAsync(() -> {
var exec = AppInstallation.ofCurrent().getCliExecutablePath().toString();
var exec = AppInstallation.ofCurrent()
.getCliExecutablePath()
.toString();
var file = DesktopShortcuts.create(exec, "open \"" + url.getValue() + "\"", name.getValue());
DesktopHelper.browseFileInDirectory(file);
});
@@ -87,6 +95,7 @@ public class ActionShortcutComp extends SimpleComp {
field.grow(true, false);
var group = new InputGroupComp(List.of(field, copyButton));
group.setHeightReference(copyButton);
group.hide(BindingsHelper.map(action, v -> !(v instanceof SerializableAction)));
return group;
}
@@ -108,6 +117,7 @@ public class ActionShortcutComp extends SimpleComp {
field.apply(struc -> struc.get().setEditable(false));
var group = new InputGroupComp(List.of(field, copyButton));
group.setHeightReference(copyButton);
group.hide(BindingsHelper.map(action, v -> !(v instanceof SerializableAction)));
return group;
}
@@ -76,6 +76,7 @@ public class AppBeaconServer {
// Not terminal!
// We can still continue without the running server
ErrorEventFactory.fromThrowable("Unable to start local http server on port " + INSTANCE.getPort(), ex)
.documentationLink(DocumentationLink.BEACON_PORT_BIND)
.build()
.handle();
}
@@ -122,7 +123,7 @@ public class AppBeaconServer {
var file = BeaconConfig.getLocalBeaconAuthFile();
var id = UUID.randomUUID().toString();
Files.writeString(file, id);
if (OsType.getLocal() != OsType.WINDOWS) {
if (OsType.ofLocal() != OsType.WINDOWS) {
Files.setPosixFilePermissions(file, PosixFilePermissions.fromString("rw-rw----"));
}
localAuthSecret = id;
@@ -1,6 +1,6 @@
package io.xpipe.app.beacon;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.core.mode.AppOperationMode;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
@@ -28,13 +28,13 @@ public class BeaconRequestHandler<T> implements HttpHandler {
@Override
public void handle(HttpExchange exchange) {
if (OperationMode.isInShutdown() && !beaconInterface.acceptInShutdown()) {
if (AppOperationMode.isInShutdown() && !beaconInterface.acceptInShutdown()) {
writeError(exchange, new BeaconClientErrorResponse("Daemon is currently in shutdown"), 400);
return;
}
if (beaconInterface.requiresCompletedStartup()) {
while (OperationMode.isInStartup()) {
while (AppOperationMode.isInStartup()) {
ThreadHelper.sleep(100);
}
}
@@ -1,7 +1,7 @@
package io.xpipe.app.beacon;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.util.ShellTemp;
import io.xpipe.app.process.ShellTemp;
import io.xpipe.beacon.BeaconClientException;
import org.apache.commons.io.FileUtils;
@@ -2,6 +2,9 @@ package io.xpipe.app.beacon.impl;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.secret.SecretManager;
import io.xpipe.app.secret.SecretQueryState;
import io.xpipe.app.terminal.TerminalView;
import io.xpipe.app.util.*;
import io.xpipe.beacon.BeaconClientException;
@@ -26,7 +26,7 @@ public class ConnectionAddExchangeImpl extends ConnectionAddExchange {
}
if (found.isPresent()) {
found.get().setStoreInternal(store, true);
DataStorage.get().updateEntryStore(found.get(), store);
return Response.builder().connection(found.get().getUuid()).build();
}
@@ -1,7 +1,7 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.ext.FixedHierarchyStore;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.FixedHierarchyStore;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionRefreshExchange;
@@ -1,6 +1,6 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.core.mode.AppOperationMode;
import io.xpipe.app.core.window.AppMainWindow;
import io.xpipe.beacon.api.DaemonFocusExchange;
@@ -9,9 +9,12 @@ import com.sun.net.httpserver.HttpExchange;
public class DaemonFocusExchangeImpl extends DaemonFocusExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) {
OperationMode.switchUp(OperationMode.GUI);
var w = AppMainWindow.getInstance();
public Object handle(HttpExchange exchange, Request msg) throws Throwable {
if (AppOperationMode.GUI.isSupported()) {
AppOperationMode.switchToSyncOrThrow(AppOperationMode.GUI);
}
var w = AppMainWindow.get();
if (w != null) {
w.focus();
}
@@ -1,6 +1,6 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.core.mode.AppOperationMode;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.DaemonModeExchange;
@@ -9,19 +9,19 @@ import com.sun.net.httpserver.HttpExchange;
public class DaemonModeExchangeImpl extends DaemonModeExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {
var mode = OperationMode.map(msg.getMode());
var mode = AppOperationMode.map(msg.getMode());
if (!mode.isSupported()) {
throw new BeaconClientException("Unsupported mode: " + msg.getMode().getDisplayName()
+ ". Supported: "
+ String.join(
", ",
OperationMode.getAll().stream()
.filter(OperationMode::isSupported)
.map(OperationMode::getId)
AppOperationMode.getAll().stream()
.filter(AppOperationMode::isSupported)
.map(AppOperationMode::getId)
.toList()));
}
OperationMode.switchToSyncIfPossible(mode);
AppOperationMode.switchToSyncIfPossible(mode);
return DaemonModeExchange.Response.builder().usedMode(msg.getMode()).build();
}
@@ -1,8 +1,8 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.core.AppOpenArguments;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.util.PlatformInit;
import io.xpipe.app.core.mode.AppOperationMode;
import io.xpipe.app.platform.PlatformInit;
import io.xpipe.beacon.BeaconServerException;
import io.xpipe.beacon.api.DaemonOpenExchange;
import io.xpipe.core.OsType;
@@ -31,11 +31,11 @@ public class DaemonOpenExchangeImpl extends DaemonOpenExchange {
// The open command is used as a default opener on Linux
// We don't want to overwrite the default startup mode
if (OsType.getLocal() == OsType.LINUX && openCounter++ == 0) {
if (OsType.ofLocal() == OsType.LINUX && openCounter++ == 0) {
return Response.builder().build();
}
OperationMode.switchToAsync(OperationMode.GUI);
AppOperationMode.switchToAsync(AppOperationMode.GUI);
} else {
AppOpenArguments.handle(msg.getArguments());
}
@@ -1,6 +1,6 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.core.mode.AppOperationMode;
import io.xpipe.beacon.api.DaemonStatusExchange;
import com.sun.net.httpserver.HttpExchange;
@@ -15,10 +15,10 @@ public class DaemonStatusExchangeImpl extends DaemonStatusExchange {
@Override
public Object handle(HttpExchange exchange, Request body) {
String mode;
if (OperationMode.get() == null) {
if (AppOperationMode.get() == null) {
mode = "none";
} else {
mode = OperationMode.get().getId();
mode = AppOperationMode.get().getId();
}
return Response.builder().mode(mode).build();
@@ -1,6 +1,6 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.core.mode.AppOperationMode;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.beacon.api.DaemonStopExchange;
@@ -17,7 +17,7 @@ public class DaemonStopExchangeImpl extends DaemonStopExchange {
public Object handle(HttpExchange exchange, Request msg) {
ThreadHelper.runAsync(() -> {
ThreadHelper.sleep(1000);
OperationMode.close();
AppOperationMode.close();
});
return Response.builder().success(true).build();
}
@@ -2,7 +2,7 @@ package io.xpipe.app.beacon.impl;
import io.xpipe.app.beacon.AppBeaconServer;
import io.xpipe.app.beacon.BlobManager;
import io.xpipe.app.util.ScriptHelper;
import io.xpipe.app.process.ScriptHelper;
import io.xpipe.beacon.api.FsScriptExchange;
import com.sun.net.httpserver.HttpExchange;
@@ -2,6 +2,7 @@ package io.xpipe.app.beacon.impl;
import io.xpipe.app.beacon.AppBeaconServer;
import io.xpipe.app.beacon.BeaconSession;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.beacon.BeaconAuthMethod;
import io.xpipe.beacon.BeaconClientException;
@@ -19,12 +20,16 @@ public class HandshakeExchangeImpl extends HandshakeExchange {
}
@Override
public Object handle(HttpExchange exchange, Request body) throws BeaconClientException {
if (!checkAuth(body.getAuth())) {
public Object handle(HttpExchange exchange, Request request) throws BeaconClientException {
if (!checkAuth(request.getAuth())) {
throw new BeaconClientException("Authentication failed");
}
var session = new BeaconSession(body.getClient(), UUID.randomUUID().toString());
TrackEvent.withTrace("Handshake request received")
.tag("client", request.getClient().toDisplayString())
.handle();
var session = new BeaconSession(request.getClient(), UUID.randomUUID().toString());
AppBeaconServer.get().addSession(session);
return Response.builder().sessionToken(session.getToken()).build();
}
@@ -9,6 +9,8 @@ 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.common.McpTransportContext;
import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.spec.HttpHeaders;
@@ -38,7 +40,11 @@ public class AppMcpServer {
@SneakyThrows
public static void init() {
var transportProvider = new HttpStreamableServerTransportProvider(
new ObjectMapper(), "/mcp", false, (req, context) -> context, null);
new JacksonMcpJsonMapper(new ObjectMapper()),
"/mcp",
false,
(serverRequest) -> McpTransportContext.EMPTY,
null);
McpSyncServer syncServer = io.modelcontextprotocol.server.McpServer.sync(transportProvider)
.serverInfo(AppNames.ofCurrent().getName(), AppProperties.get().getVersion())
@@ -6,11 +6,10 @@ 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.common.McpTransportContext;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.json.TypeRef;
import io.modelcontextprotocol.server.McpTransportContextExtractor;
import io.modelcontextprotocol.spec.*;
import io.modelcontextprotocol.util.Assert;
@@ -32,8 +31,6 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
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";
@@ -46,7 +43,7 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
private final boolean disallowDelete;
private final ObjectMapper objectMapper;
private final McpJsonMapper jsonMapper;
private final ConcurrentHashMap<String, McpStreamableServerSession> sessions = new ConcurrentHashMap<>();
@@ -58,16 +55,16 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
private KeepAliveScheduler keepAliveScheduler;
HttpStreamableServerTransportProvider(
ObjectMapper objectMapper,
McpJsonMapper jsonMapper,
String mcpEndpoint,
boolean disallowDelete,
McpTransportContextExtractor<HttpExchange> contextExtractor,
Duration keepAliveInterval) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
Assert.notNull(jsonMapper, "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.jsonMapper = jsonMapper;
this.mcpEndpoint = mcpEndpoint;
this.disallowDelete = disallowDelete;
this.contextExtractor = contextExtractor;
@@ -85,7 +82,8 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
}
public List<String> protocolVersions() {
return List.of(ProtocolVersions.MCP_2024_11_05, ProtocolVersions.MCP_2025_03_26);
return List.of(
ProtocolVersions.MCP_2024_11_05, ProtocolVersions.MCP_2025_03_26, ProtocolVersions.MCP_2025_06_18);
}
@Override
@@ -186,8 +184,7 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
logger.debug("Handling GET request for session: {}", sessionId);
McpTransportContext transportContext =
this.contextExtractor.extract(exchange, new DefaultMcpTransportContext());
McpTransportContext transportContext = this.contextExtractor.extract(exchange);
try {
exchange.getResponseHeaders().add("Content-Type", TEXT_EVENT_STREAM);
@@ -265,13 +262,12 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
badRequestErrors.add("application/json required in Accept header");
}
McpTransportContext transportContext =
this.contextExtractor.extract(exchange, new DefaultMcpTransportContext());
McpTransportContext transportContext = this.contextExtractor.extract(exchange);
try {
var body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body);
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(jsonMapper, body);
// Handle initialization request
if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest
@@ -283,7 +279,7 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
}
McpSchema.InitializeRequest initializeRequest =
objectMapper.convertValue(jsonrpcRequest.params(), new TypeReference<>() {});
jsonMapper.convertValue(jsonrpcRequest.params(), new TypeRef<>() {});
McpStreamableServerSession.McpStreamableServerSessionInit init =
this.sessionFactory.startSession(initializeRequest);
this.sessions.put(init.session().getId(), init.session());
@@ -291,7 +287,7 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
try {
McpSchema.InitializeResult initResult = init.initResult().block();
String jsonResponse = objectMapper.writeValueAsString(new McpSchema.JSONRPCResponse(
String jsonResponse = jsonMapper.writeValueAsString(new McpSchema.JSONRPCResponse(
McpSchema.JSONRPC_VERSION, jsonrpcRequest.id(), initResult, null));
var jsonBytes = jsonResponse.getBytes(StandardCharsets.UTF_8);
@@ -399,8 +395,7 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
return;
}
McpTransportContext transportContext =
this.contextExtractor.extract(exchange, new DefaultMcpTransportContext());
McpTransportContext transportContext = this.contextExtractor.extract(exchange);
if (exchange.getRequestHeaders().getFirst(HttpHeaders.MCP_SESSION_ID) == null) {
sendError(exchange, 400, "Session ID required in mcp-session-id header");
@@ -462,6 +457,11 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
logger.debug("Streamable session transport {} initialized with SSE writer", sessionId);
}
@Override
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
return jsonMapper.convertValue(data, typeRef);
}
@Override
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message, String messageId) {
return Mono.fromRunnable(() -> {
@@ -477,7 +477,7 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
return;
}
String jsonText = objectMapper.writeValueAsString(message);
String jsonText = jsonMapper.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);
@@ -523,10 +523,5 @@ public class HttpStreamableServerTransportProvider implements McpStreamableServe
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);
}
}
}
@@ -5,7 +5,9 @@ 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.FileInfo;
import io.xpipe.app.ext.SingletonSessionStore;
import io.xpipe.app.process.ScriptHelper;
import io.xpipe.app.process.ShellControl;
import io.xpipe.app.process.TerminalInitScriptConfig;
import io.xpipe.app.process.WorkingDirectoryFunction;
@@ -13,13 +15,12 @@ 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.json.jackson.JacksonMcpJsonMapper;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;
import lombok.Builder;
@@ -124,7 +125,9 @@ public final class McpTools {
object.set("found", json);
return McpSchema.CallToolResult.builder()
.structuredContent(JacksonMapper.getDefault().writeValueAsString(object))
.structuredContent(
new JacksonMcpJsonMapper(JacksonMapper.getDefault()),
JacksonMapper.getDefault().writeValueAsString(object))
.build();
}))
.build();
@@ -229,13 +232,13 @@ public final class McpTools {
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");
if (!fs.fileExists(path) && !fs.directoryExists(path)) {
throw new BeaconClientException("Path " + path + " does not exist");
}
var entry = fs.getFileInfo(path);
if (entry.isEmpty()) {
throw new BeaconClientException("File " + path + " does not exist");
throw new BeaconClientException("Path " + path + " does not exist");
}
var map = new LinkedHashMap<String, Object>();
@@ -13,11 +13,11 @@ import io.xpipe.app.ext.FileSystemStore;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.hub.comp.StoreEntryWrapper;
import io.xpipe.app.hub.comp.StoreViewState;
import io.xpipe.app.platform.BindingsHelper;
import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.FileReference;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.FilePath;
@@ -78,7 +78,7 @@ public class BrowserFileChooserSessionComp extends ModalOverlayContentComp {
modal.addButton(new ModalButton("select", () -> model.finishChooser(), true, true));
modal.show();
ThreadHelper.runAsync(() -> {
model.openFileSystemAsync(store.get(), (sc) -> initialPath.get(), null);
model.openFileSystemAsync(store.get(), null, (sc) -> initialPath.get(), null);
});
}
@@ -108,7 +108,7 @@ public class BrowserFileChooserSessionComp extends ModalOverlayContentComp {
}
if (entry.getStore() instanceof ShellStore) {
model.openFileSystemAsync(entry.ref(), null, busy);
model.openFileSystemAsync(entry.ref(), null, null, busy);
}
});
};
@@ -2,6 +2,7 @@ package io.xpipe.app.browser;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.ext.FileSystem;
import io.xpipe.app.ext.FileSystemStore;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope;
@@ -80,6 +81,7 @@ public class BrowserFileChooserSessionModel extends BrowserAbstractSessionModel<
public void openFileSystemAsync(
DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<DataStoreEntryRef<FileSystemStore>, FileSystem, Exception> customFileSystemFactory,
FailableFunction<BrowserFileSystemTabModel, FilePath, Exception> path,
BooleanProperty externalBusy) {
if (store == null) {
@@ -91,7 +93,13 @@ public class BrowserFileChooserSessionModel extends BrowserAbstractSessionModel<
try (var ignored =
new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
model = new BrowserFileSystemTabModel(this, store, selectionMode);
model = new BrowserFileSystemTabModel(
this,
store,
selectionMode,
customFileSystemFactory != null
? customFileSystemFactory
: ref -> ref.getStore().createFileSystem());
model.init();
// Prevent multiple calls from interfering with each other
synchronized (BrowserFileChooserSessionModel.this) {
@@ -12,8 +12,8 @@ import io.xpipe.app.core.window.AppMainWindow;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.hub.comp.StoreEntryWrapper;
import io.xpipe.app.hub.comp.StoreViewState;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.platform.BindingsHelper;
import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
@@ -75,7 +75,7 @@ public class BrowserFullSessionComp extends SimpleComp {
loadingStack.apply(struc -> struc.get().setPickOnBounds(false));
var delayedStack = new DelayedInitComp(
left, () -> StoreViewState.get() != null && StoreViewState.get().isInitialized());
delayedStack.hide(AppMainWindow.getInstance().getStage().widthProperty().lessThan(1000));
delayedStack.hide(AppMainWindow.get().getStage().widthProperty().lessThan(1000));
var splitPane = new LeftSplitPaneComp(delayedStack, loadingStack)
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
.withOnDividerChange(d -> {
@@ -4,6 +4,8 @@ import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.file.BrowserHistorySavedState;
import io.xpipe.app.browser.file.BrowserHistoryTabModel;
import io.xpipe.app.browser.file.BrowserTransferModel;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.ext.FileSystem;
import io.xpipe.app.ext.FileSystemStore;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.prefs.AppPrefs;
@@ -61,7 +63,10 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
DEFAULT.openSync(new BrowserHistoryTabModel(DEFAULT), null);
if (AppPrefs.get().pinLocalMachineOnStartup().get()) {
var tab = new BrowserFileSystemTabModel(
DEFAULT, DataStorage.get().local().ref(), BrowserFileSystemTabModel.SelectionMode.ALL);
DEFAULT,
DataStorage.get().local().ref(),
BrowserFileSystemTabModel.SelectionMode.ALL,
ref -> ref.getStore().createFileSystem());
try {
DEFAULT.openSync(tab, null);
DEFAULT.pinTab(tab);
@@ -150,7 +155,7 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
}
}
public void unpinTab(BrowserSessionTab tab) {
public void unpinTab() {
ThreadHelper.runFailableAsync(() -> {
globalPinnedTab.setValue(null);
});
@@ -170,7 +175,7 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
public void restoreStateAsync(BrowserHistorySavedState.Entry e, BooleanProperty busy) {
var storageEntry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
storageEntry.ifPresent(entry -> {
openFileSystemAsync(entry.ref(), model -> e.getPath(), busy);
openFileSystemAsync(entry.ref(), null, model -> e.getPath(), busy);
});
}
@@ -202,6 +207,7 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
public void openFileSystemAsync(
DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<DataStoreEntryRef<FileSystemStore>, FileSystem, Exception> customFileSystemFactory,
FailableFunction<BrowserFileSystemTabModel, FilePath, Exception> path,
BooleanProperty externalBusy) {
if (store == null) {
@@ -209,12 +215,13 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
}
ThreadHelper.runFailableAsync(() -> {
openFileSystemSync(store, path, externalBusy, true);
openFileSystemSync(store, customFileSystemFactory, path, externalBusy, true);
});
}
public BrowserFileSystemTabModel openFileSystemSync(
DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<DataStoreEntryRef<FileSystemStore>, FileSystem, Exception> customFileSystemFactory,
FailableFunction<BrowserFileSystemTabModel, FilePath, Exception> path,
BooleanProperty externalBusy,
boolean select)
@@ -223,12 +230,19 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
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 = new BrowserFileSystemTabModel(
this,
store,
BrowserFileSystemTabModel.SelectionMode.ALL,
customFileSystemFactory != null
? customFileSystemFactory
: ref -> ref.getStore().createFileSystem());
model.init();
// Prevent multiple calls from interfering with each other
synchronized (BrowserFullSessionModel.this) {
sessionEntries.add(model);
if (select) {
AppLayoutModel.get().selectBrowser();
// The tab pane doesn't automatically select new tabs
selectedEntry.setValue(model);
}
@@ -8,11 +8,11 @@ import io.xpipe.app.comp.base.StackComp;
import io.xpipe.app.core.App;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.platform.ContextMenuHelper;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ContextMenuHelper;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.app.util.PlatformThread;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
@@ -211,7 +211,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
for (var a : c.getAddedSubList()) {
PlatformThread.runLaterIfNeeded(() -> {
try (var b = new BooleanScope(addingTab).start()) {
try (var ignored = new BooleanScope(addingTab).start()) {
var t = createTab(tabs, a);
map.put(a, t);
tabs.getTabs().add(t);
@@ -307,7 +307,7 @@ public class BrowserSessionTabsComp extends SimpleComp {
},
model.getGlobalPinnedTab())));
unpin.setOnAction(event -> {
model.unpinTab(tabModel);
model.unpinTab();
event.consume();
});
cm.getItems().add(unpin);
@@ -511,6 +511,8 @@ public class BrowserSessionTabsComp extends SimpleComp {
var color = tabModel.getColor();
if (color != null) {
c.getStyleClass().add(color.getId());
} else {
c.getStyleClass().add("gray");
}
c.addEventHandler(DragEvent.DRAG_ENTERED, de -> {
// Prevent switch when dragging local files into app
@@ -45,6 +45,7 @@ public abstract class BrowserAction extends StoreAction<FileSystemStore> {
} else {
model = BrowserFullSessionModel.DEFAULT.openFileSystemSync(
ref.asNeeded(),
null,
model -> {
return getTargetDirectory(model);
},
@@ -57,8 +58,8 @@ public abstract class BrowserAction extends StoreAction<FileSystemStore> {
model.getBusy().set(true);
// Start shell in case we exited
model.getFileSystem().getShell().orElseThrow().start();
// Restart in case we exited
model.getFileSystem().reinitIfNeeded();
return true;
}
@@ -11,8 +11,4 @@ public interface BrowserActionProvider extends ActionProvider {
default boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return true;
}
default boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return true;
}
}
@@ -2,13 +2,14 @@ package io.xpipe.app.browser.action.impl;
import io.xpipe.app.action.AbstractAction;
import io.xpipe.app.action.ActionProvider;
import io.xpipe.app.browser.file.BrowserFileInput;
import io.xpipe.app.browser.file.BrowserFileOutput;
import io.xpipe.app.storage.DataStorage;
import lombok.NonNull;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -27,7 +28,7 @@ public class ApplyFileEditActionProvider implements ActionProvider {
String target;
@NonNull
InputStream input;
BrowserFileInput input;
@NonNull
BrowserFileOutput output;
@@ -36,9 +37,13 @@ public class ApplyFileEditActionProvider implements ActionProvider {
public void executeImpl() throws Exception {
output.beforeTransfer();
try (var out = output.open()) {
input.transferTo(out);
input.open().transferTo(out);
}
try {
output.onFinish();
} finally {
input.onFinish();
}
output.onFinish();
}
@Override
@@ -50,7 +55,14 @@ public class ApplyFileEditActionProvider implements ActionProvider {
public Map<String, String> toDisplayMap() {
var map = new LinkedHashMap<String, String>();
map.put("Action", getDisplayName());
map.put("Target", target);
var system = output.target();
if (system.isPresent()) {
map.put("System", DataStorage.get().getStoreEntryDisplayName(system.get()));
}
map.put("File", target);
return map;
}
}
@@ -4,9 +4,9 @@ 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.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.process.LocalShell;
import io.xpipe.app.process.ShellControl;
import io.xpipe.app.util.DesktopHelper;
import io.xpipe.app.util.LocalShell;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
@@ -17,6 +17,10 @@ public class BrowseInNativeManagerActionProvider implements BrowserActionProvide
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
if (model.getFileSystem().getShell().isEmpty()) {
return false;
}
return model.getFileSystem()
.getShell()
.orElseThrow()
@@ -39,10 +43,7 @@ public class BrowseInNativeManagerActionProvider implements BrowserActionProvide
for (BrowserEntry entry : getEntries()) {
var e = entry.getRawFileEntry().getPath();
var localFile = sc.getLocalSystemAccess().translateToLocalSystemPath(e);
try (var local = LocalShell.getShell().start()) {
DesktopHelper.browsePathRemote(
local, localFile, entry.getRawFileEntry().getKind());
}
DesktopHelper.browseFileInDirectory(localFile.asLocalPath());
}
}
}
@@ -4,8 +4,6 @@ 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.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.core.OsType;
import lombok.NonNull;
import lombok.experimental.SuperBuilder;
@@ -17,8 +15,7 @@ public class ChgrpActionProvider implements BrowserActionProvider {
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var os = model.getFileSystem().getShell().orElseThrow().getOsType();
return os != OsType.WINDOWS && os != OsType.MACOS;
return model.getFileSystem().supportsChgrp();
}
@Override
@@ -37,19 +34,9 @@ public class ChgrpActionProvider implements BrowserActionProvider {
@Override
public void executeImpl() throws Exception {
model.getFileSystem()
.getShell()
.orElseThrow()
.executeSimpleCommand(CommandBuilder.of()
.add("chgrp")
.addIf(recursive, "-R")
.addLiteral(group)
.addFiles(getEntries().stream()
.map(browserEntry -> browserEntry
.getRawFileEntry()
.getPath()
.toString())
.toList()));
for (BrowserEntry entry : getEntries()) {
model.getFileSystem().chgrp(entry.getRawFileEntry().getPath(), group, recursive);
}
model.refreshBrowserEntriesSync(getEntries());
}
@@ -4,8 +4,6 @@ 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.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.core.OsType;
import lombok.NonNull;
import lombok.experimental.SuperBuilder;
@@ -17,7 +15,7 @@ public class ChmodActionProvider implements BrowserActionProvider {
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS;
return model.getFileSystem().supportsChmod();
}
@Override
@@ -36,19 +34,9 @@ public class ChmodActionProvider implements BrowserActionProvider {
@Override
public void executeImpl() throws Exception {
model.getFileSystem()
.getShell()
.orElseThrow()
.executeSimpleCommand(CommandBuilder.of()
.add("chmod")
.addIf(recursive, "-R")
.addLiteral(permissions)
.addFiles(getEntries().stream()
.map(browserEntry -> browserEntry
.getRawFileEntry()
.getPath()
.toString())
.toList()));
for (BrowserEntry entry : getEntries()) {
model.getFileSystem().chmod(entry.getRawFileEntry().getPath(), permissions, recursive);
}
model.refreshBrowserEntriesSync(getEntries());
}
@@ -4,8 +4,6 @@ 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.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.core.OsType;
import lombok.NonNull;
import lombok.experimental.SuperBuilder;
@@ -17,8 +15,7 @@ public class ChownActionProvider implements BrowserActionProvider {
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var os = model.getFileSystem().getShell().orElseThrow().getOsType();
return os != OsType.WINDOWS && os != OsType.MACOS;
return model.getFileSystem().supportsChown();
}
@Override
@@ -37,19 +34,9 @@ public class ChownActionProvider implements BrowserActionProvider {
@Override
public void executeImpl() throws Exception {
model.getFileSystem()
.getShell()
.orElseThrow()
.executeSimpleCommand(CommandBuilder.of()
.add("chown")
.addIf(recursive, "-R")
.addLiteral(owner)
.addFiles(getEntries().stream()
.map(browserEntry -> browserEntry
.getRawFileEntry()
.getPath()
.toString())
.toList()));
for (BrowserEntry entry : getEntries()) {
model.getFileSystem().chown(entry.getRawFileEntry().getPath(), owner, recursive);
}
model.refreshBrowserEntriesSync(getEntries());
}
@@ -3,11 +3,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.core.FileKind;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.ext.FileKind;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
public class ComputeDirectorySizesActionProvider implements BrowserActionProvider {
@Override
@@ -15,6 +18,11 @@ public class ComputeDirectorySizesActionProvider implements BrowserActionProvide
return "computeDirectorySizes";
}
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return model.getFileSystem().supportsDirectorySizes();
}
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@@ -3,7 +3,7 @@ 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.core.FileKind;
import io.xpipe.app.ext.FileKind;
import lombok.NonNull;
import lombok.experimental.SuperBuilder;
@@ -3,7 +3,7 @@ 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.core.FileKind;
import io.xpipe.app.ext.FileKind;
import lombok.NonNull;
import lombok.experimental.SuperBuilder;
@@ -3,7 +3,7 @@ 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.core.FileKind;
import io.xpipe.app.ext.FileKind;
import io.xpipe.core.FilePath;
import lombok.NonNull;
@@ -4,7 +4,7 @@ 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.browser.file.BrowserFileSystemTabModel;
import io.xpipe.core.FileKind;
import io.xpipe.app.ext.FileKind;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
@@ -5,7 +5,7 @@ import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileOpener;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.core.FileKind;
import io.xpipe.app.ext.FileKind;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
@@ -4,10 +4,10 @@ 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.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.ext.FileKind;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.LocalShell;
import io.xpipe.app.process.ShellControl;
import io.xpipe.app.util.LocalShell;
import io.xpipe.core.FileKind;
import io.xpipe.core.OsType;
import lombok.experimental.SuperBuilder;
@@ -24,6 +24,10 @@ public class OpenFileNativeDetailsActionProvider implements BrowserActionProvide
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
if (model.getFileSystem().getShell().isEmpty()) {
return false;
}
var sc = model.getFileSystem().getShell().orElseThrow();
return sc.getLocalSystemAccess().supportsFileSystemAccess();
}
@@ -38,7 +42,7 @@ public class OpenFileNativeDetailsActionProvider implements BrowserActionProvide
for (BrowserEntry entry : getEntries()) {
var e = entry.getRawFileEntry().getPath();
var localFile = sc.getLocalSystemAccess().translateToLocalSystemPath(e);
switch (OsType.getLocal()) {
switch (OsType.ofLocal()) {
case OsType.Windows ignored -> {
var shell = LocalShell.getLocalPowershell();
if (shell.isEmpty()) {
@@ -59,7 +63,8 @@ public class OpenFileNativeDetailsActionProvider implements BrowserActionProvide
// as
// long as the parent process is running.
// So let's keep one process running
shell.get().command(content).notComplex().execute();
// Ignore exit value as this can fail somehow (maybe if the system blocks shell com objects?)
shell.get().command(content).notComplex().executeAndCheck();
}
case OsType.Linux ignored -> {
var dbus = String.format(
@@ -0,0 +1,49 @@
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.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.ext.FileKind;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.LocalShell;
import io.xpipe.app.process.ShellControl;
import io.xpipe.app.util.DesktopHelper;
import io.xpipe.core.OsType;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
public class OpenFileNativeManagerActionProvider implements BrowserActionProvider {
@Override
public String getId() {
return "openFileNativeManager";
}
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
if (model.getFileSystem().getShell().isEmpty()) {
return false;
}
var sc = model.getFileSystem().getShell().orElseThrow();
return sc.getLocalSystemAccess().supportsFileSystemAccess();
}
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@Override
public void executeImpl() throws Exception {
ShellControl sc = model.getFileSystem().getShell().get();
for (BrowserEntry entry : getEntries()) {
var e = entry.getRawFileEntry().getPath();
var localFile = sc.getLocalSystemAccess().translateToLocalSystemPath(e);
DesktopHelper.browseFileInDirectory(localFile.asLocalPath());
}
}
}
}
@@ -5,7 +5,7 @@ import io.xpipe.app.browser.action.BrowserActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileOpener;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.core.FileKind;
import io.xpipe.app.ext.FileKind;
import io.xpipe.core.OsType;
import lombok.experimental.SuperBuilder;
@@ -22,7 +22,7 @@ public class OpenFileWithActionProvider implements BrowserActionProvider {
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return OsType.getLocal() == OsType.WINDOWS
return OsType.ofLocal() == OsType.WINDOWS
&& entries.size() == 1
&& entries.stream().allMatch(entry -> entry.getRawFileEntry().getKind() == FileKind.FILE);
}
@@ -1,56 +0,0 @@
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.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.FileKind;
import io.xpipe.core.FilePath;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.Collections;
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 {
@Override
public void executeImpl() throws Exception {
var entries = getEntries();
var dirs = entries.size() > 0
? entries.stream()
.map(browserEntry -> browserEntry.getRawFileEntry().getPath())
.toList()
: model.getCurrentDirectory() != null
? List.of(model.getCurrentDirectory().getPath())
: Collections.singletonList((FilePath) null);
for (var dir : dirs) {
var name = (dir != null ? dir + " - " : "") + model.getName().getValue();
model.openTerminalSync(
name, dir, model.getFileSystem().getShell().orElseThrow(), dirs.size() == 1);
}
}
}
}
@@ -2,6 +2,8 @@ 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.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.process.ProcessOutputException;
@@ -10,6 +12,7 @@ import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
@@ -20,6 +23,11 @@ public class RunCommandInBackgroundActionProvider implements BrowserActionProvid
return "runFileInBackground";
}
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return model.getFileSystem().getShell().isPresent();
}
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@@ -48,7 +56,7 @@ public class RunCommandInBackgroundActionProvider implements BrowserActionProvid
// Only throw actual error output
if (exitCode != 0) {
throw ErrorEventFactory.expected(ProcessOutputException.of(exitCode, out.get(), err.get()));
throw ErrorEventFactory.expected(ProcessOutputException.of(command, exitCode, out.get(), err.get()));
}
}
@@ -2,6 +2,8 @@ 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.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.util.CommandDialog;
import lombok.NonNull;
@@ -9,6 +11,7 @@ import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class RunCommandInBrowserActionProvider implements BrowserActionProvider {
@@ -18,6 +21,11 @@ public class RunCommandInBrowserActionProvider implements BrowserActionProvider
return "runCommandInBrowser";
}
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return model.getFileSystem().getShell().isPresent();
}
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@@ -2,12 +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.browser.file.BrowserFileSystemTabModel;
import lombok.NonNull;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class RunCommandInTerminalActionProvider implements BrowserActionProvider {
@@ -17,6 +20,11 @@ public class RunCommandInTerminalActionProvider implements BrowserActionProvider
return "runCommandInTerminal";
}
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return model.getFileSystem().getShell().isPresent();
}
@Jacksonized
@SuperBuilder
public static class Action extends BrowserAction {
@@ -1,25 +1,31 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.process.OsFileSystem;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.util.GlobalTimer;
import io.xpipe.core.FilePath;
import javafx.css.PseudoClass;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.Label;
import javafx.scene.input.DragEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.Region;
import javafx.util.Callback;
import atlantafx.base.controls.Breadcrumbs;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
public class BrowserBreadcrumbBar extends SimpleComp {
private final BrowserFileSystemTabModel model;
private Instant lastHoverUpdate;
public BrowserBreadcrumbBar(BrowserFileSystemTabModel model) {
this.model = model;
@@ -27,23 +33,54 @@ public class BrowserBreadcrumbBar extends SimpleComp {
@Override
protected Region createSimple() {
Callback<Breadcrumbs.BreadCrumbItem<String>, ButtonBase> crumbFactory = crumb -> {
var name = crumb.getValue().equals("/")
Callback<Breadcrumbs.BreadCrumbItem<FilePath>, ButtonBase> crumbFactory = crumb -> {
var name = crumb.getValue().toString().equals("/")
? "/"
: FilePath.of(crumb.getValue()).getFileName();
: crumb.getValue().getFileName();
var btn = new Button(name, null);
btn.setMnemonicParsing(false);
btn.setFocusTraversable(false);
btn.setOnDragEntered(event -> onDragEntered(btn, crumb.getValue()));
btn.setOnDragOver(event -> onDragOver(event));
btn.setOnDragExited(event -> onDragExited(btn));
return btn;
};
return createBreadcrumbs(crumbFactory, null);
}
private Region createBreadcrumbs(
Callback<Breadcrumbs.BreadCrumbItem<String>, ButtonBase> crumbFactory,
Callback<Breadcrumbs.BreadCrumbItem<String>, ? extends Node> dividerFactory) {
private void onDragEntered(Button button, FilePath path) {
button.pseudoClassStateChanged(PseudoClass.getPseudoClass("hover"), true);
var breadcrumbs = new Breadcrumbs<String>();
var timestamp = Instant.now();
lastHoverUpdate = timestamp;
// Reduce printed window updates
GlobalTimer.delay(
() -> {
if (!timestamp.equals(lastHoverUpdate)) {
return;
}
model.cdAsync(path);
},
Duration.ofMillis(500));
}
private void onDragOver(DragEvent event) {
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
event.consume();
}
private void onDragExited(Button button) {
button.pseudoClassStateChanged(PseudoClass.getPseudoClass("hover"), false);
lastHoverUpdate = null;
}
private Region createBreadcrumbs(
Callback<Breadcrumbs.BreadCrumbItem<FilePath>, ButtonBase> crumbFactory,
Callback<Breadcrumbs.BreadCrumbItem<FilePath>, ? extends Node> dividerFactory) {
var breadcrumbs = new Breadcrumbs<FilePath>();
breadcrumbs.setMinWidth(0);
model.getCurrentPath().subscribe(val -> {
PlatformThread.runLaterIfNeeded(() -> {
@@ -52,30 +89,21 @@ public class BrowserBreadcrumbBar extends SimpleComp {
return;
}
var sc = model.getFileSystem().getShell();
if (sc.isEmpty()) {
breadcrumbs.setDividerFactory(item -> item != null && !item.isLast() ? new Label("/") : null);
} else {
breadcrumbs.setDividerFactory(item -> {
if (item == null) {
return null;
}
breadcrumbs.setDividerFactory(item -> {
if (item == null) {
return null;
}
if (item.isFirst() && item.getValue().equals("/")) {
return new Label("");
}
if (item.isFirst() && item.getValue().toString().equals("/")) {
return new Label("");
}
return new Label(OsFileSystem.of(sc.get().getOsType()).getFileSystemSeparator());
});
}
return new Label(model.getFileSystem().getFileSeparator());
});
var elements = createBreadcumbHierarchy(val);
var modifiedElements = new ArrayList<>(elements);
if (val.toString().startsWith("/")) {
modifiedElements.addFirst("/");
}
Breadcrumbs.BreadCrumbItem<String> items =
Breadcrumbs.buildTreeModel(modifiedElements.toArray(String[]::new));
var elements = createBreadcrumbHierarchy(val);
Breadcrumbs.BreadCrumbItem<FilePath> items =
Breadcrumbs.buildTreeModel(elements.toArray(FilePath[]::new));
breadcrumbs.setSelectedCrumb(items);
});
});
@@ -94,19 +122,24 @@ public class BrowserBreadcrumbBar extends SimpleComp {
return breadcrumbs;
}
private List<String> createBreadcumbHierarchy(FilePath filePath) {
var f = filePath.toString() + "/";
var list = new ArrayList<String>();
private List<FilePath> createBreadcrumbHierarchy(FilePath filePath) {
var f = filePath.toDirectory().toString();
var list = new ArrayList<FilePath>();
int lastElementStart = 0;
for (int i = 0; i < f.length(); i++) {
if (f.charAt(i) == '\\' || f.charAt(i) == '/') {
if (i - lastElementStart > 0) {
list.add(f.substring(0, i));
list.add(FilePath.of(f.substring(0, i)).toDirectory());
}
lastElementStart = i + 1;
}
}
if (filePath.toString().startsWith("/")) {
list.addFirst(FilePath.of("/"));
}
return list;
}
}
@@ -3,8 +3,8 @@ package io.xpipe.app.browser.file;
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 io.xpipe.app.platform.GlobalClipboard;
import io.xpipe.app.platform.GlobalObjectProperty;
import javafx.beans.property.Property;
import javafx.scene.input.ClipboardContent;
@@ -4,8 +4,8 @@ import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.hub.comp.*;
import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
@@ -4,9 +4,9 @@ import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.FilterComp;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.hub.comp.DataStoreCategoryChoiceComp;
import io.xpipe.app.hub.comp.StoreCategoryWrapper;
import io.xpipe.app.hub.comp.StoreViewState;
import io.xpipe.app.util.DataStoreCategoryChoiceComp;
import javafx.beans.property.Property;
import javafx.scene.layout.Region;
@@ -4,7 +4,7 @@ import io.xpipe.app.action.ActionProvider;
import io.xpipe.app.browser.menu.BrowserMenuCategory;
import io.xpipe.app.browser.menu.BrowserMenuItemProvider;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.util.InputHelper;
import io.xpipe.app.platform.InputHelper;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.SeparatorMenuItem;
@@ -10,7 +10,7 @@ import javafx.beans.property.SimpleObjectProperty;
public class BrowserDialogs {
public static FileConflictChoice showFileConflictAlert(FilePath file, boolean multiple) {
public static FileConflictChoice showFileConflictDialog(FilePath file, boolean multiple) {
var choice = new SimpleObjectProperty<FileConflictChoice>();
var key = multiple ? "fileConflictAlertContentMultiple" : "fileConflictAlertContent";
var w = multiple ? 1050 : 400;
@@ -3,7 +3,7 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.browser.icon.BrowserIconDirectoryType;
import io.xpipe.app.browser.icon.BrowserIconFileType;
import io.xpipe.app.ext.FileEntry;
import io.xpipe.core.FileKind;
import io.xpipe.app.ext.FileKind;
import lombok.Getter;
@@ -64,7 +64,7 @@ public class BrowserEntry {
if (fileType != null) {
return fileType.getIcon();
} else if (directoryType != null) {
return directoryType.getIcon(rawFileEntry);
return directoryType.getIcon();
} else {
return rawFileEntry != null && rawFileEntry.resolved().getKind() == FileKind.DIRECTORY
? "browser/default_folder.svg"
@@ -0,0 +1,37 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.ext.FileSystem;
import io.xpipe.core.FilePath;
import java.util.regex.Pattern;
public class BrowserFileDuplicates {
public static FilePath renameFileDuplicate(FileSystem fileSystem, FilePath target, boolean dir) throws Exception {
// Who has more than 10 copies?
for (int i = 0; i < 10; i++) {
target = renameFile(target);
if ((dir && !fileSystem.directoryExists(target)) || (!dir && !fileSystem.fileExists(target))) {
return target;
}
}
return target;
}
private static FilePath renameFile(FilePath target) {
var name = target.getFileName();
var pattern = Pattern.compile("(.+)\\((\\d+)\\)\\.(.+?)");
var matcher = pattern.matcher(name);
if (matcher.matches()) {
try {
var number = Integer.parseInt(matcher.group(2));
var newFile = target.getParent().join(matcher.group(1) + "(" + (number + 1) + ")." + matcher.group(3));
return newFile;
} catch (NumberFormatException ignored) {
}
}
var ext = target.getExtension();
return FilePath.of(target.getBaseName() + "(" + 1 + ")" + (ext.isPresent() ? "." + ext.get() : ""));
}
}
@@ -0,0 +1,141 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.ext.ConnectionFileSystem;
import io.xpipe.app.ext.FileEntry;
import io.xpipe.app.ext.FileInfo;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.ElevationFunction;
import io.xpipe.core.FilePath;
import io.xpipe.core.OsType;
import java.io.InputStream;
public interface BrowserFileInput {
static BrowserFileInput openFileInput(BrowserFileSystemTabModel model, FileEntry file) throws Exception {
if (model.isClosed()) {
return BrowserFileInput.none();
}
var defOutput = createFileInputImpl(model, file, false);
if (model.getFileSystem().getShell().isEmpty()) {
return defOutput;
}
var sc = model.getFileSystem().getShell().orElseThrow();
var requiresSudo =
sc.getOsType() != OsType.WINDOWS && requiresSudo(model, (FileInfo.Unix) file.getInfo(), file.getPath());
if (!requiresSudo) {
return defOutput;
}
var elevate = AppDialog.confirm("fileReadSudo");
if (!elevate) {
return defOutput;
}
var rootOutput = createFileInputImpl(model, file, true);
return rootOutput;
}
private static boolean requiresSudo(BrowserFileSystemTabModel model, FileInfo.Unix info, FilePath filePath)
throws Exception {
if (model.getFileSystem().getShell().isEmpty()) {
return false;
}
var sc = model.getFileSystem().getShell().get();
if (sc.view().isRoot()) {
return false;
}
if (info != null) {
var otherWrite = info.getPermissions().charAt(6) == 'r';
if (otherWrite) {
return false;
}
var userOwned = info.getUid() != null
&& sc.view().getPasswdFile().getUidForUser(sc.view().user()) == info.getUid()
|| info.getUser() != null && sc.view().user().equals(info.getUser());
var userWrite = info.getPermissions().charAt(0) == 'r';
if (userOwned && userWrite) {
return false;
}
}
var test = model.getFileSystem()
.getShell()
.orElseThrow()
.command(CommandBuilder.of().add("test", "-r").addFile(filePath))
.executeAndCheck();
return !test;
}
private static BrowserFileInput createFileInputImpl(
BrowserFileSystemTabModel model, FileEntry file, boolean elevate) throws Exception {
var shell = model.getFileSystem().getShell();
var sc = shell.isEmpty()
? null
: elevate
? shell.orElseThrow()
.identicalDialectSubShell()
.elevated(ElevationFunction.elevated(null))
.start()
: model.getFileSystem().getShell().orElseThrow().start();
var fs = elevate ? new ConnectionFileSystem(sc) : model.getFileSystem();
var output = new BrowserFileInput() {
@Override
public InputStream open() throws Exception {
try {
return fs.openInput(file.getPath());
} catch (Exception ex) {
if (elevate) {
fs.close();
}
throw ex;
}
}
@Override
public void onFinish() throws Exception {
if (elevate) {
fs.close();
}
}
};
return output;
}
static BrowserFileInput none() {
return new BrowserFileInput() {
@Override
public InputStream open() {
return null;
}
@Override
public void onFinish() {}
};
}
static BrowserFileInput of(InputStream in) {
return new BrowserFileInput() {
@Override
public InputStream open() {
return in;
}
@Override
public void onFinish() {}
};
}
InputStream open() throws Exception;
void onFinish() throws Exception;
}
@@ -5,10 +5,10 @@ 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.ext.FileInfo;
import io.xpipe.app.ext.FileKind;
import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.util.*;
import io.xpipe.core.FileInfo;
import io.xpipe.core.FileKind;
import io.xpipe.core.OsType;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
@@ -24,6 +24,7 @@ import javafx.scene.input.*;
import javafx.scene.layout.Region;
import atlantafx.base.theme.Styles;
import lombok.SneakyThrows;
import java.time.Duration;
import java.time.Instant;
@@ -146,17 +147,21 @@ public final class BrowserFileListComp extends SimpleComp {
table.setAccessibleText("Directory contents");
var placeholder = new Label();
var placeholderText = Bindings.createStringBinding(() -> {
if (fileList.getFileSystemModel().getCurrentPath().get() == null) {
return null;
}
var placeholderText = Bindings.createStringBinding(
() -> {
if (fileList.getFileSystemModel().getCurrentPath().get() == null) {
return null;
}
if (fileList.getFileSystemModel().getBusy().get()) {
return null;
}
if (fileList.getFileSystemModel().getBusy().get()) {
return null;
}
return AppI18n.get("emptyDirectory");
}, AppI18n.activeLanguage(), fileList.getFileSystemModel().getBusy(), fileList.getFileSystemModel().getCurrentPath());
return AppI18n.get("emptyDirectory");
},
AppI18n.activeLanguage(),
fileList.getFileSystemModel().getBusy(),
fileList.getFileSystemModel().getCurrentPath());
placeholder.textProperty().bind(PlatformThread.sync(placeholderText));
table.setPlaceholder(placeholder);
AppFontSizes.base(placeholder);
@@ -189,18 +194,12 @@ public final class BrowserFileListComp extends SimpleComp {
TableColumn<BrowserEntry, String> modeCol,
TableColumn<BrowserEntry, String> ownerCol,
TableColumn<BrowserEntry, String> sizeCol) {
var os = fileList.getFileSystemModel()
.getFileSystem()
.getShell()
.map(shellControl -> shellControl.getOsType())
.orElse(null);
table.widthProperty().subscribe((newValue) -> {
if (os != OsType.WINDOWS && os != OsType.MACOS) {
if (fileList.getFileSystemModel().getFileSystem().supportsOwnerColumn()) {
ownerCol.setVisible(newValue.doubleValue() > 1000);
}
var shell = fileList.getFileSystemModel().getFileSystem().getShell().orElseThrow();
if (!OsType.WINDOWS.equals(shell.getOsType()) && !OsType.MACOS.equals(shell.getOsType())) {
if (fileList.getFileSystemModel().getFileSystem().supportsModeColumn()) {
modeCol.setVisible(newValue.doubleValue() > 600);
}
@@ -222,6 +221,7 @@ public final class BrowserFileListComp extends SimpleComp {
return Math.max(200, tableView.getWidth() - sum);
}
@SneakyThrows
private String formatOwner(BrowserEntry param) {
FileInfo.Unix unix = param.getRawFileEntry().resolved().getInfo() instanceof FileInfo.Unix u ? u : null;
if (unix == null) {
@@ -233,20 +233,35 @@ public final class BrowserFileListComp extends SimpleComp {
}
var m = fileList.getFileSystemModel();
var v = m.getFileSystem().getShell().isPresent()
? m.getFileSystem().getShell().get().view()
: null;
var user = unix.getUser() != null
? unix.getUser()
: m.getCache().getUsers().getOrDefault(unix.getUid(), "?");
: v != null ? v.getPasswdFile().getUsers().getOrDefault(unix.getUid(), "?") : null;
var group = unix.getGroup() != null
? unix.getGroup()
: m.getCache().getGroups().getOrDefault(unix.getGid(), "?");
var uid = String.valueOf(
unix.getUid() != null ? unix.getUid() : m.getCache().getUidForUser(user));
var gid = String.valueOf(
unix.getGid() != null ? unix.getGid() : m.getCache().getGidForGroup(group));
if (uid.equals(gid) && user.equals(group)) {
return user + " [" + uid + "]";
: v != null ? v.getGroupFile().getGroups().getOrDefault(unix.getGid(), "?") : null;
var uid = unix.getUid() != null
? String.valueOf(unix.getUid())
: v != null ? v.getPasswdFile().getUidForUser(user) : null;
var gid = unix.getGid() != null
? String.valueOf(unix.getGid())
: v != null ? v.getGroupFile().getGidForGroup(group) : null;
var userFormat = user + (uid != null ? " [" + uid + "]" : "");
var groupFormat = group + (gid != null ? " [" + gid + "]" : "");
if (uid != null && uid.equals(gid) && user != null && user.equals(group)) {
return userFormat;
}
return user + " [" + uid + "] / " + group + " [" + gid + "]";
if (uid == null && gid == null && user != null && user.equals(group)) {
return userFormat;
}
return userFormat + " / " + groupFormat;
}
private void prepareTypedSelectionModel(TableView<BrowserEntry> table) {
@@ -394,7 +409,7 @@ public final class BrowserFileListComp extends SimpleComp {
var selected = fileList.getSelection();
var action = BrowserMenuProviders.getFlattened(fileList.getFileSystemModel(), selected).stream()
.filter(browserAction -> browserAction.isApplicable(fileList.getFileSystemModel(), selected)
&& browserAction.isActive(fileList.getFileSystemModel(), selected))
&& browserAction.isActive(fileList.getFileSystemModel()))
.filter(browserAction -> browserAction.getShortcut() != null)
.filter(browserAction -> browserAction.getShortcut().match(event))
.findAny();
@@ -585,13 +600,15 @@ public final class BrowserFileListComp extends SimpleComp {
ownerCol.setPrefWidth(0);
}
var shell =
fileList.getFileSystemModel().getFileSystem().getShell().orElseThrow();
if (OsType.WINDOWS.equals(shell.getOsType()) || OsType.MACOS.equals(shell.getOsType())) {
if (!fileList.getFileSystemModel().getFileSystem().supportsModeColumn()) {
modeCol.setVisible(false);
ownerCol.setVisible(false);
} else {
modeCol.setVisible(table.getWidth() > 600);
}
if (!fileList.getFileSystemModel().getFileSystem().supportsOwnerColumn()) {
ownerCol.setVisible(false);
} else {
if (table.getWidth() > 1000) {
ownerCol.setVisible(hasOwner);
} else if (!hasOwner) {
@@ -1,13 +1,14 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.core.AppSystemInfo;
import io.xpipe.app.ext.FileKind;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.GlobalTimer;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.FileKind;
import io.xpipe.core.OsType;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.TableView;
@@ -16,8 +17,10 @@ import javafx.scene.input.*;
import lombok.Getter;
import java.io.IOException;
import java.nio.file.InvalidPathException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Objects;
@@ -29,8 +32,7 @@ public class BrowserFileListCompEntry {
private final BrowserEntry item;
private final BrowserFileListModel model;
private Point2D lastOver = new Point2D(-1, -1);
private Runnable activeTask;
private Instant lastHoverUpdate;
private ContextMenu lastContextMenu;
public BrowserFileListCompEntry(
@@ -148,6 +150,22 @@ public class BrowserFileListCompEntry {
private boolean acceptsDrop(DragEvent event) {
// Accept drops from outside the app window
if (event.getGestureSource() == null) {
// Don't accept 7zip temp files
if (OsType.ofLocal() == OsType.WINDOWS
&& event.getDragboard().getFiles().stream().anyMatch(file -> {
try {
return file.toPath()
.toRealPath()
.startsWith(
AppSystemInfo.ofWindows().getTemp())
&& file.toPath().getFileName().toString().matches("7z[A-Z0-9]+");
} catch (IOException ignored) {
return false;
}
})) {
return false;
}
return true;
}
@@ -237,6 +255,7 @@ public class BrowserFileListCompEntry {
} else {
model.getDraggedOverEmpty().setValue(false);
}
lastHoverUpdate = null;
event.consume();
}
@@ -273,48 +292,45 @@ public class BrowserFileListCompEntry {
}
}
private void acceptDrag(DragEvent event) {
model.getDraggedOverEmpty()
.setValue(item == null || item.getRawFileEntry().getKind() != FileKind.DIRECTORY);
model.getDraggedOverDirectory().setValue(item);
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
}
private void handleHoverTimer(DragEvent event) {
if (item == null || item.getRawFileEntry().getKind() != FileKind.DIRECTORY) {
return;
}
if (lastOver.getX() == event.getX() && lastOver.getY() == event.getY()) {
return;
}
lastOver = (new Point2D(event.getX(), event.getY()));
activeTask = new Runnable() {
@Override
public void run() {
if (activeTask != this) {
return;
}
if (item != model.getDraggedOverDirectory().getValue()) {
return;
}
model.getFileSystemModel()
.cdAsync(item.getRawFileEntry().getPath().toString());
}
};
GlobalTimer.delayAsync(activeTask, Duration.ofMillis(1200));
}
public void onDragEntered(DragEvent event) {
event.consume();
if (!acceptsDrop(event)) {
return;
}
acceptDrag(event);
model.getDraggedOverEmpty()
.setValue(item == null || item.getRawFileEntry().getKind() != FileKind.DIRECTORY);
model.getDraggedOverDirectory().setValue(item);
if (item != null) {
var timestamp = Instant.now();
lastHoverUpdate = timestamp;
// Reduce printed window updates
GlobalTimer.delay(
() -> {
if (!timestamp.equals(lastHoverUpdate)) {
return;
}
if (item != model.getDraggedOverDirectory().getValue()) {
return;
}
model.getFileSystemModel()
.cdAsync(item.getRawFileEntry().getPath());
},
Duration.ofMillis(500));
}
}
public void onDragOver(DragEvent event) {
event.consume();
if (!acceptsDrop(event)) {
return;
}
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
event.consume();
}
@SuppressWarnings("unchecked")
@@ -333,14 +349,4 @@ public class BrowserFileListCompEntry {
row.getParent().getParent().getParent().getParent());
tv.getSelectionModel().select(item);
}
public void onDragOver(DragEvent event) {
event.consume();
if (!acceptsDrop(event)) {
return;
}
acceptDrag(event);
handleHoverTimer(event);
}
}
@@ -5,8 +5,8 @@ import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.comp.base.TooltipHelper;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.InputHelper;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.platform.InputHelper;
import io.xpipe.app.platform.PlatformThread;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
@@ -51,8 +51,7 @@ public class BrowserFileListFilterComp extends Comp<BrowserFileListFilterComp.St
Tooltip.install(
button,
TooltipHelper.create(
AppI18n.observable("app.search"),
new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN)));
AppI18n.observable("search"), new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN)));
text.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue && filterString.getValue() == null) {
if (button.isFocused()) {
@@ -2,11 +2,10 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.browser.action.impl.MoveFileActionProvider;
import io.xpipe.app.ext.FileEntry;
import io.xpipe.app.ext.FileKind;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.FileKind;
import io.xpipe.core.FilePath;
import io.xpipe.core.OsType;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
@@ -128,10 +127,7 @@ public final class BrowserFileListModel {
// This check will fail on case-insensitive file systems when changing the case of the file
// So skip it in this case
var skipExistCheck =
(fileSystemModel.getFileSystem().getShell().orElseThrow().getOsType() == OsType.WINDOWS ||
fileSystemModel.getFileSystem().getShell().orElseThrow().getOsType() == OsType.MACOS)
&& old.getFileName().equalsIgnoreCase(newName);
var skipExistCheck = old.getFileName().equalsIgnoreCase(newName);
if (!skipExistCheck) {
boolean exists;
try {
@@ -2,12 +2,12 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.comp.base.LazyTextFieldComp;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.ext.FileKind;
import io.xpipe.app.platform.ContextMenuHelper;
import io.xpipe.app.platform.InputHelper;
import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ContextMenuHelper;
import io.xpipe.app.util.InputHelper;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.FileKind;
import io.xpipe.core.FilePath;
import javafx.application.Platform;
@@ -2,164 +2,19 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.ext.ConnectionFileSystem;
import io.xpipe.app.ext.FileEntry;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.ElevationFunction;
import io.xpipe.app.process.ProcessOutputException;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.FileBridge;
import io.xpipe.app.util.FileOpener;
import io.xpipe.core.FileInfo;
import io.xpipe.core.FilePath;
import io.xpipe.core.OsType;
import io.xpipe.app.util.HumanReadableFormat;
import lombok.SneakyThrows;
import java.io.OutputStream;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
public class BrowserFileOpener {
private static BrowserFileOutput openFileOutput(BrowserFileSystemTabModel model, FileEntry file, long totalBytes)
throws Exception {
var fileSystem = model.getFileSystem();
if (model.isClosed() || fileSystem.getShell().isEmpty()) {
return BrowserFileOutput.none();
}
if (totalBytes == 0) {
var existingSize = model.getFileSystem().getFileSize(file.getPath());
if (existingSize != 0) {
var blank = AppDialog.confirm(
"fileWriteBlankTitle", AppI18n.observable("fileWriteBlankContent", file.getPath()));
if (!blank) {
return BrowserFileOutput.none();
}
}
}
var sc = model.getFileSystem().getShell().orElseThrow();
var requiresSudo =
sc.getOsType() != OsType.WINDOWS && requiresSudo(model, (FileInfo.Unix) file.getInfo(), file.getPath());
var defOutput = createFileOutput(model, file, totalBytes, false);
if (!requiresSudo) {
return defOutput;
}
var elevate = AppDialog.confirm("fileWriteSudo");
if (!elevate) {
return defOutput;
}
var rootOutput = createFileOutput(model, file, totalBytes, true);
return rootOutput;
}
private static boolean requiresSudo(BrowserFileSystemTabModel model, FileInfo.Unix info, FilePath filePath)
throws Exception {
if (model.getCache().isRoot()) {
return false;
}
if (info != null) {
var otherWrite = info.getPermissions().charAt(7) == 'w';
if (otherWrite) {
return false;
}
var userOwned = info.getUid() != null
&& model.getCache().getUidForUser(model.getCache().getUsername()) == info.getUid()
|| info.getUser() != null && model.getCache().getUsername().equals(info.getUser());
var userWrite = info.getPermissions().charAt(1) == 'w';
if (userOwned && userWrite) {
return false;
}
}
var test = model.getFileSystem()
.getShell()
.orElseThrow()
.command(CommandBuilder.of().add("test", "-w").addFile(filePath))
.executeAndCheck();
return !test;
}
private static BrowserFileOutput createFileOutput(
BrowserFileSystemTabModel model, FileEntry file, long totalBytes, boolean elevate) throws Exception {
var sc = elevate
? model.getFileSystem()
.getShell()
.orElseThrow()
.identicalDialectSubShell()
.elevated(ElevationFunction.elevated(null))
.start()
: model.getFileSystem().getShell().orElseThrow().start();
var fs = elevate ? new ConnectionFileSystem(sc) : model.getFileSystem();
var isSudoersFile = file.getPath().startsWith("/etc/sudo");
var output = new BrowserFileOutput() {
@Override
public Optional<DataStoreEntry> target() {
return Optional.of(model.getEntry().get());
}
@Override
public boolean hasOutput() {
return true;
}
@Override
public OutputStream open() throws Exception {
try {
return fs.openOutput(file.getPath(), totalBytes);
} catch (Exception ex) {
if (elevate) {
fs.close();
}
throw ex;
}
}
@Override
public void beforeTransfer() throws Exception {
if (isSudoersFile) {
fs.copy(file.getPath(), sc.getSystemTemporaryDirectory().join(file.getName()));
}
}
@Override
public void onFinish() throws Exception {
if (isSudoersFile) {
if (sc.view().findProgram("visudo").isPresent()) {
try {
sc.command(CommandBuilder.of()
.add("visudo", "-c", "-f")
.addFile(file.getPath()))
.execute();
} catch (ProcessOutputException ex) {
ErrorEventFactory.fromThrowable(ex).expected().handle();
fs.copy(sc.getSystemTemporaryDirectory().join(file.getName()), file.getPath());
}
}
}
if (elevate) {
fs.close();
}
model.refreshFileEntriesSync(List.of(file));
}
};
return output;
}
@SneakyThrows
private static int calculateKey(BrowserFileSystemTabModel model, FileEntry entry) {
// Use different key for empty / non-empty files to prevent any issues from blanked files when transfer fails
@@ -168,7 +23,8 @@ public class BrowserFileOpener {
}
public static void openWithAnyApplication(BrowserFileSystemTabModel model, FileEntry entry) {
if (model.getFileSystem().getShell().orElseThrow().isLocal()) {
if (model.getFileSystem().getShell().isPresent()
&& model.getFileSystem().getShell().get().isLocal()) {
FileOpener.openWithAnyApplication(entry.getPath().toString());
return;
}
@@ -180,44 +36,14 @@ public class BrowserFileOpener {
file.getFileName(),
key,
new BooleanScope(model.getBusy()).exclusive(),
() -> {
return entry.getFileSystem().openInput(file);
},
(size) -> {
if (model.isClosed()) {
return BrowserFileOutput.none();
}
return new BrowserFileOutput() {
@Override
public Optional<DataStoreEntry> target() {
return Optional.of(model.getEntry().get());
}
@Override
public boolean hasOutput() {
return true;
}
@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));
}
};
},
() -> BrowserFileInput.openFileInput(model, entry),
(size) -> BrowserFileOutput.openFileOutput(model, entry, size),
s -> FileOpener.openWithAnyApplication(s));
}
public static void openInDefaultApplication(BrowserFileSystemTabModel model, FileEntry entry) {
if (model.getFileSystem().getShell().orElseThrow().isLocal()) {
if (model.getFileSystem().getShell().isPresent()
&& model.getFileSystem().getShell().get().isLocal()) {
FileOpener.openInDefaultApplication(entry.getPath().toString());
return;
}
@@ -229,39 +55,8 @@ public class BrowserFileOpener {
file.getFileName(),
key,
new BooleanScope(model.getBusy()).exclusive(),
() -> {
return entry.getFileSystem().openInput(file);
},
(size) -> {
if (model.isClosed()) {
return BrowserFileOutput.none();
}
return new BrowserFileOutput() {
@Override
public Optional<DataStoreEntry> target() {
return Optional.of(model.getEntry().get());
}
@Override
public boolean hasOutput() {
return true;
}
@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));
}
};
},
() -> BrowserFileInput.openFileInput(model, entry),
(size) -> BrowserFileOutput.openFileOutput(model, entry, size),
s -> FileOpener.openInDefaultApplication(s));
}
@@ -270,11 +65,23 @@ public class BrowserFileOpener {
if (editor == null) {
return;
}
if (model.getFileSystem().getShell().orElseThrow().isLocal()) {
if (model.getFileSystem().getShell().isPresent()
&& model.getFileSystem().getShell().get().isLocal()) {
FileOpener.openInTextEditor(entry.getPath().toString());
return;
}
var size = entry.getFileSizeLong().orElse(0L);
if (size > 1_000_000) {
var confirm = AppDialog.confirm(
"largeFileWarningTitle",
AppI18n.observable("largeFileWarningContent", HumanReadableFormat.byteCount(size)));
if (!confirm) {
return;
}
}
var file = entry.getPath();
var key = calculateKey(model, entry);
FileBridge.get()
@@ -282,11 +89,9 @@ public class BrowserFileOpener {
file.getFileName(),
key,
new BooleanScope(model.getBusy()).exclusive(),
() -> {
return entry.getFileSystem().openInput(file);
},
(size) -> {
return openFileOutput(model, entry, size);
() -> BrowserFileInput.openFileInput(model, entry),
(os) -> {
return BrowserFileOutput.openFileOutput(model, entry, os);
},
FileOpener::openInTextEditor);
}
@@ -1,12 +1,167 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.ext.ConnectionFileSystem;
import io.xpipe.app.ext.FileEntry;
import io.xpipe.app.ext.FileInfo;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.ElevationFunction;
import io.xpipe.app.process.ProcessOutputException;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.FilePath;
import io.xpipe.core.OsType;
import java.io.OutputStream;
import java.util.List;
import java.util.Optional;
public interface BrowserFileOutput {
static BrowserFileOutput openFileOutput(BrowserFileSystemTabModel model, FileEntry file, long totalBytes)
throws Exception {
if (model.isClosed()) {
return BrowserFileOutput.none();
}
if (totalBytes == 0) {
var existingSize = model.getFileSystem().getFileSize(file.getPath());
if (existingSize != 0) {
var blank = AppDialog.confirm(
"fileWriteBlankTitle", AppI18n.observable("fileWriteBlankContent", file.getPath()));
if (!blank) {
return BrowserFileOutput.none();
}
}
}
var defOutput = createFileOutputImpl(model, file, totalBytes, false);
if (model.getFileSystem().getShell().isEmpty()) {
return defOutput;
}
var sc = model.getFileSystem().getShell().orElseThrow();
var requiresSudo =
sc.getOsType() != OsType.WINDOWS && requiresSudo(model, (FileInfo.Unix) file.getInfo(), file.getPath());
if (!requiresSudo) {
return defOutput;
}
var elevate = AppDialog.confirm("fileWriteSudo");
if (!elevate) {
return defOutput;
}
var rootOutput = createFileOutputImpl(model, file, totalBytes, true);
return rootOutput;
}
private static boolean requiresSudo(BrowserFileSystemTabModel model, FileInfo.Unix info, FilePath filePath)
throws Exception {
if (model.getFileSystem().getShell().isEmpty()) {
return false;
}
var sc = model.getFileSystem().getShell().get();
if (sc.view().isRoot()) {
return false;
}
if (info != null) {
var otherWrite = info.getPermissions().charAt(7) == 'w';
if (otherWrite) {
return false;
}
var userOwned = info.getUid() != null
&& sc.view().getPasswdFile().getUidForUser(sc.view().user()) == info.getUid()
|| info.getUser() != null && sc.view().user().equals(info.getUser());
var userWrite = info.getPermissions().charAt(1) == 'w';
if (userOwned && userWrite) {
return false;
}
}
var test = model.getFileSystem()
.getShell()
.orElseThrow()
.command(CommandBuilder.of().add("test", "-w").addFile(filePath))
.executeAndCheck();
return !test;
}
private static BrowserFileOutput createFileOutputImpl(
BrowserFileSystemTabModel model, FileEntry file, long totalBytes, boolean elevate) throws Exception {
var shell = model.getFileSystem().getShell();
var sc = shell.isEmpty()
? null
: elevate
? shell.orElseThrow()
.identicalDialectSubShell()
.elevated(ElevationFunction.elevated(null))
.start()
: model.getFileSystem().getShell().orElseThrow().start();
var fs = elevate ? new ConnectionFileSystem(sc) : model.getFileSystem();
var checkSudoersFile = shell.isPresent() && file.getPath().startsWith("/etc/sudo");
var output = new BrowserFileOutput() {
@Override
public Optional<DataStoreEntry> target() {
return Optional.of(model.getEntry().get());
}
@Override
public boolean hasOutput() {
return true;
}
@Override
public OutputStream open() throws Exception {
try {
return fs.openOutput(file.getPath(), totalBytes);
} catch (Exception ex) {
if (elevate) {
fs.close();
}
throw ex;
}
}
@Override
public void beforeTransfer() throws Exception {
if (checkSudoersFile) {
fs.copy(file.getPath(), sc.getSystemTemporaryDirectory().join(file.getName()));
}
}
@Override
public void onFinish() throws Exception {
if (checkSudoersFile) {
if (sc.view().findProgram("visudo").isPresent()) {
try {
sc.command(CommandBuilder.of()
.add("visudo", "-c", "-f")
.addFile(file.getPath()))
.execute();
} catch (ProcessOutputException ex) {
ErrorEventFactory.fromThrowable(ex).expected().handle();
fs.copy(sc.getSystemTemporaryDirectory().join(file.getName()), file.getPath());
}
}
}
if (elevate) {
fs.close();
}
model.refreshFileEntriesSync(List.of(file));
}
};
return output;
}
static BrowserFileOutput none() {
return new BrowserFileOutput() {
@@ -3,7 +3,6 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.browser.icon.BrowserIcons;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.augment.GrowAugment;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.ext.FileEntry;
@@ -42,7 +41,7 @@ public class BrowserFileOverviewComp extends SimpleComp {
event.consume();
});
l.setAlignment(Pos.CENTER_LEFT);
GrowAugment.create(true, false).augment(l);
l.setMaxWidth(10000);
return l;
});
};
@@ -5,9 +5,9 @@ import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.core.AppStyle;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.core.window.AppWindowStyle;
import io.xpipe.app.platform.BindingsHelper;
import io.xpipe.app.platform.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
@@ -42,7 +42,7 @@ public class BrowserFileSelectionListComp extends SimpleComp {
public static Image snapshot(ObservableList<BrowserEntry> list) {
var r = new BrowserFileSelectionListComp(list).styleClass("drag").createRegion();
var scene = new Scene(r);
AppWindowHelper.setupStylesheets(scene);
AppWindowStyle.addStylesheets(scene);
AppStyle.addStylesheets(scene);
SnapshotParameters parameters = new SnapshotParameters();
parameters.setFill(Color.TRANSPARENT);
@@ -1,77 +0,0 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.ShellControl;
import io.xpipe.app.process.ShellDialect;
import io.xpipe.app.util.PasswdFile;
import io.xpipe.app.util.ShellControlCache;
import io.xpipe.core.OsType;
import lombok.Getter;
import java.util.LinkedHashMap;
import java.util.Map;
@Getter
public class BrowserFileSystemCache extends ShellControlCache {
private final BrowserFileSystemTabModel model;
private final String username;
private final PasswdFile passwdFile;
private final Map<Integer, String> groups = new LinkedHashMap<>();
public BrowserFileSystemCache(BrowserFileSystemTabModel model) throws Exception {
super(model.getFileSystem().getShell().orElseThrow());
this.model = model;
ShellControl sc = model.getFileSystem().getShell().get();
ShellDialect d = sc.getShellDialect();
// If there is no id command, we should still be fine with just assuming root
username = d.printUsernameCommand(sc).readStdoutIfPossible().orElse("root");
passwdFile = PasswdFile.parse(sc);
loadGroups();
}
public Map<Integer, String> getUsers() {
return passwdFile.getUsers();
}
public int getUidForUser(String name) {
return passwdFile.getUidForUser(name);
}
public int getGidForGroup(String name) {
return groups.entrySet().stream()
.filter(e -> e.getValue().equals(name))
.findFirst()
.map(e -> e.getKey())
.orElse(0);
}
private void loadGroups() throws Exception {
var sc = model.getFileSystem().getShell().orElseThrow();
if (sc.getOsType() == OsType.WINDOWS || sc.getOsType() == OsType.MACOS) {
return;
}
var lines = sc.command(CommandBuilder.of().add("cat").addFile("/etc/group"))
.sensitive()
.readStdoutIfPossible()
.orElse("");
lines.lines().forEach(s -> {
var split = s.split(":");
try {
groups.putIfAbsent(Integer.parseInt(split[2]), split[0]);
} catch (Exception ignored) {
}
});
if (groups.isEmpty()) {
groups.put(0, "root");
}
}
public boolean isRoot() {
return username.equals("root");
}
}
@@ -1,9 +1,9 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.ext.FileEntry;
import io.xpipe.app.ext.FileKind;
import io.xpipe.app.ext.FileSystem;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.core.FileKind;
import io.xpipe.core.FilePath;
import io.xpipe.core.OsType;
@@ -94,24 +94,18 @@ public class BrowserFileSystemHelper {
return resolved.toDirectory();
}
public static void validateDirectoryPath(BrowserFileSystemTabModel model, FilePath path, boolean verifyExists)
throws Exception {
public static void validateDirectoryPath(FileSystem fs, FilePath path, boolean verifyExists) throws Exception {
if (path == null) {
return;
}
var shell = model.getFileSystem().getShell();
if (shell.isEmpty()) {
return;
}
if (verifyExists && !model.getFileSystem().directoryExists(path)) {
if (verifyExists && !fs.directoryExists(path)) {
throw ErrorEventFactory.expected(new IllegalArgumentException(
String.format("Directory %s does not exist or is not accessible", path)));
}
try {
model.getFileSystem().directoryAccessible(path);
fs.directoryAccessible(path);
} catch (Exception ex) {
ErrorEventFactory.expected(ex);
throw ex;
@@ -9,12 +9,13 @@ import io.xpipe.app.comp.augment.ContextMenuAugment;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.platform.InputHelper;
import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.util.GlobalTimer;
import io.xpipe.app.util.InputHelper;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.core.FilePath;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
@@ -93,8 +94,13 @@ public class BrowserFileSystemTabComp extends SimpleComp {
refreshBtn.managedProperty().bind(smallWidth.not());
refreshBtn.visibleProperty().bind(refreshBtn.managedProperty());
terminalBtn.managedProperty().bind(smallWidth.not());
terminalBtn.visibleProperty().bind(terminalBtn.managedProperty());
var terminalSupported =
BrowserMenuProviders.byId("openInTerminal", model, List.of()).isApplicable(model, List.of());
terminalBtn.managedProperty().bind(smallWidth.not().and(new ReadOnlyBooleanWrapper(terminalSupported)));
terminalBtn
.visibleProperty()
.bind(terminalBtn.managedProperty().and(new ReadOnlyBooleanWrapper(terminalSupported)));
var filter = new BrowserFileListFilterComp(model, model.getFilter())
.hide(smallWidth)
@@ -135,7 +141,7 @@ public class BrowserFileSystemTabComp extends SimpleComp {
if (fullSessionModel.getGlobalPinnedTab().getValue() != model) {
fullSessionModel.pinTab(model);
} else {
fullSessionModel.unpinTab(model);
fullSessionModel.unpinTab();
}
e.consume();
});
@@ -215,6 +221,8 @@ public class BrowserFileSystemTabComp extends SimpleComp {
}
var fileList = new VerticalComp(fileListElements)
.styleClass("browser-content")
.styleClass("color-box")
.styleClass("gray")
.apply(struc -> {
struc.get().focusedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
@@ -9,10 +9,10 @@ import io.xpipe.app.browser.menu.BrowserMenuItemProvider;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.core.window.AppMainWindow;
import io.xpipe.app.ext.FileEntry;
import io.xpipe.app.ext.FileKind;
import io.xpipe.app.ext.FileSystem;
import io.xpipe.app.ext.FileSystemStore;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.ext.WrapperFileSystem;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.process.*;
@@ -20,12 +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.FileKind;
import io.xpipe.core.FailableFunction;
import io.xpipe.core.FilePath;
import io.xpipe.core.OsType;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@@ -48,7 +49,11 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
private final BrowserFileListModel fileList;
private final ReadOnlyObjectWrapper<FilePath> currentPath = new ReadOnlyObjectWrapper<>();
private final BrowserFileSystemHistory history = new BrowserFileSystemHistory();
private final BooleanProperty inOverview = new SimpleBooleanProperty();
private final ObservableBooleanValue inOverview = Bindings.createBooleanBinding(
() -> {
return currentPath.get() == null;
},
currentPath);
private final ObservableList<UUID> terminalRequests = FXCollections.observableArrayList();
private final BooleanProperty transferCancelled = new SimpleBooleanProperty();
private final Property<BrowserTransferProgress> progress = new SimpleObjectProperty<>();
@@ -56,21 +61,18 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
FXCollections.observableArrayList();
private final LongProperty progressTransferSpeed = new SimpleLongProperty();
private final Property<Duration> progressRemaining = new SimpleObjectProperty<>();
private final FailableFunction<DataStoreEntryRef<FileSystemStore>, FileSystem, Exception> fileSystemFactory;
private FileSystem fileSystem;
private BrowserFileSystemSavedState savedState;
private BrowserFileSystemCache cache;
public BrowserFileSystemTabModel(
BrowserAbstractSessionModel<?> model,
DataStoreEntryRef<? extends FileSystemStore> entry,
SelectionMode selectionMode) {
SelectionMode selectionMode,
FailableFunction<DataStoreEntryRef<FileSystemStore>, FileSystem, Exception> fileSystemFactory) {
super(model, entry);
this.inOverview.bind(Bindings.createBooleanBinding(
() -> {
return currentPath.get() == null;
},
currentPath));
fileList = new BrowserFileListModel(selectionMode, this);
this.fileList = new BrowserFileListModel(selectionMode, this);
this.fileSystemFactory = fileSystemFactory;
}
public void updateProgress(BrowserTransferProgress n) {
@@ -148,23 +150,26 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
@Override
public void init() throws Exception {
BooleanScope.executeExclusive(busy, () -> {
var fs = entry.getStore().createFileSystem();
var fs = fileSystemFactory.apply(getEntry().asNeeded());
if (fs.getShell().isPresent()) {
ProcessControlProvider.get().withDefaultScripts(fs.getShell().get());
var originalFs = fs;
fs = new WrapperFileSystem(
originalFs, () -> originalFs.getShell().get().isRunning(true));
}
fs.open();
// Listen to kill after init as the shell might get killed during init for certain reasons
if (fs.getShell().isPresent()) {
fs.getShell().get().onKill(() -> {
if (fs.getRawShellControl().isPresent()) {
fs.getRawShellControl().get().onKill(() -> {
browserModel.closeAsync(this);
});
}
this.fileSystem = fs;
this.cache = new BrowserFileSystemCache(this);
// Cache for later usage
if (fs.getShell().isPresent()) {
fs.getShell().get().view().getPasswdFile();
fs.getShell().get().view().getGroupFile();
}
for (var a : ActionProvider.ALL) {
if (a instanceof BrowserMenuItemProvider ba) {
ba.init(this);
@@ -198,13 +203,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
}
public void startIfNeeded() throws Exception {
var s = fileSystem.getShell();
if (s.isPresent()) {
s.get().start();
if (s.get().isAnyStreamClosed()) {
s.get().restart();
}
}
fileSystem.reinitIfNeeded();
}
public void killTransfer() {
@@ -300,11 +299,11 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
return false;
}
if (OsType.getLocal() != OsType.WINDOWS) {
if (OsType.ofLocal() != OsType.WINDOWS) {
return false;
}
if (AppMainWindow.getInstance().getStage().getWidth() <= 1380) {
if (AppMainWindow.get().getStage().getWidth() <= 1380) {
return false;
}
@@ -330,6 +329,10 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
}
public Optional<String> cdSyncOrRetry(String path, boolean customInput) {
if (!fileSystem.isRunning()) {
return Optional.empty();
}
var cps = currentPath.get() != null ? currentPath.get().toString() : null;
if (Objects.equals(path, cps)) {
return Optional.empty();
@@ -421,7 +424,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
}
try {
BrowserFileSystemHelper.validateDirectoryPath(this, resolvedPath, true);
BrowserFileSystemHelper.validateDirectoryPath(fileSystem, resolvedPath, true);
cdSyncWithoutCheck(resolvedPath);
} catch (Exception ex) {
ErrorEventFactory.fromThrowable(ex).handle();
@@ -499,6 +502,20 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
});
}
public void duplicateFile(FileEntry entry) {
// Technically we would have to create an action to allow confirmations for this
// But in practice, this is almost a non mutable action, so we will save the effort
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
startIfNeeded();
var adjusted = BrowserFileDuplicates.renameFileDuplicate(
fileSystem, entry.getPath(), entry.getKind() == FileKind.DIRECTORY);
fileSystem.copy(entry.getPath(), adjusted);
refreshSync();
});
});
}
public boolean isClosed() {
return false;
}
@@ -559,6 +576,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
}
@Getter
@SuppressWarnings("unused")
public enum SelectionMode {
SINGLE_FILE(false, true, false),
MULTIPLE_FILE(true, true, false),
@@ -1,12 +1,11 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.core.mode.AppOperationMode;
import io.xpipe.app.ext.FileEntry;
import io.xpipe.app.ext.FileKind;
import io.xpipe.app.ext.FileSystem;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.process.OsFileSystem;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.FileKind;
import io.xpipe.core.FilePath;
import javafx.beans.property.BooleanProperty;
@@ -25,7 +24,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.regex.Pattern;
public class BrowserFileTransferOperation {
@@ -82,17 +80,9 @@ public class BrowserFileTransferOperation {
return new BrowserFileTransferOperation(target, entries, transferMode, checkConflicts, progress, cancelled);
}
private void restartShellIfNeeded() throws Exception {
var source = getFiles().getFirst().getFileSystem().getShell().orElseThrow();
var target = getTarget().getFileSystem().getShell().orElseThrow();
if (source.isAnyStreamClosed()) {
source.restart();
}
if (target.isAnyStreamClosed()) {
target.restart();
}
private void reinitFileSystemsIfNeeded() throws Exception {
getFiles().getFirst().getFileSystem().reinitIfNeeded();
getTarget().getFileSystem().reinitIfNeeded();
}
private void updateProgress(BrowserTransferProgress progress) {
@@ -118,7 +108,7 @@ public class BrowserFileTransferOperation {
return BrowserDialogs.FileConflictChoice.SKIP;
}
var choice = BrowserDialogs.showFileConflictAlert(target, multiple);
var choice = BrowserDialogs.showFileConflictDialog(target, multiple);
if (choice == BrowserDialogs.FileConflictChoice.CANCEL) {
lastConflictChoice = BrowserDialogs.FileConflictChoice.CANCEL;
return BrowserDialogs.FileConflictChoice.CANCEL;
@@ -149,7 +139,7 @@ public class BrowserFileTransferOperation {
}
private boolean cancelled() {
return cancelled.get() || OperationMode.isInShutdown();
return cancelled.get() || AppOperationMode.isInShutdown();
}
public boolean isMove() {
@@ -169,7 +159,7 @@ public class BrowserFileTransferOperation {
return;
}
restartShellIfNeeded();
reinitFileSystemsIfNeeded();
cancelled.set(false);
@@ -186,13 +176,12 @@ public class BrowserFileTransferOperation {
handleSingleOnSameFileSystem(file);
} else {
// Transfers might change the working directory
var currentDir =
file.getFileSystem().getShell().orElseThrow().view().pwd();
var currentDir = file.getFileSystem().pwd();
handleSingleAcrossFileSystems(file);
// Expect a kill
if (!file.getFileSystem().getShell().orElseThrow().isAnyStreamClosed()) {
file.getFileSystem().getShell().orElseThrow().view().cd(currentDir);
if (currentDir.isPresent() && !file.getFileSystem().requiresReinit()) {
file.getFileSystem().cd(currentDir.get());
}
}
}
@@ -224,7 +213,8 @@ public class BrowserFileTransferOperation {
if (sourceFile.equals(targetFile)) {
// Duplicate file by renaming it
targetFile = renameFileLoop(target.getFileSystem(), targetFile, source.getKind() == FileKind.DIRECTORY);
targetFile = BrowserFileDuplicates.renameFileDuplicate(
target.getFileSystem(), targetFile, source.getKind() == FileKind.DIRECTORY);
}
if (source.getKind() == FileKind.DIRECTORY && target.getFileSystem().directoryExists(targetFile)) {
@@ -240,7 +230,8 @@ public class BrowserFileTransferOperation {
}
if (fileConflictChoice == BrowserDialogs.FileConflictChoice.RENAME) {
targetFile = renameFileLoop(target.getFileSystem(), targetFile, source.getKind() == FileKind.DIRECTORY);
targetFile = BrowserFileDuplicates.renameFileDuplicate(
target.getFileSystem(), targetFile, source.getKind() == FileKind.DIRECTORY);
}
}
@@ -252,40 +243,14 @@ public class BrowserFileTransferOperation {
}
}
private FilePath renameFileLoop(FileSystem fileSystem, FilePath target, boolean dir) throws Exception {
// Who has more than 10 copies?
for (int i = 0; i < 10; i++) {
target = renameFile(target);
if ((dir && !fileSystem.directoryExists(target)) || (!dir && !fileSystem.fileExists(target))) {
return target;
}
}
return target;
}
private FilePath renameFile(FilePath target) {
var name = target.getFileName();
var pattern = Pattern.compile("(.+) \\((\\d+)\\)\\.(.+?)");
var matcher = pattern.matcher(name);
if (matcher.matches()) {
try {
var number = Integer.parseInt(matcher.group(2));
var newFile = target.getParent().join(matcher.group(1) + " (" + (number + 1) + ")." + matcher.group(3));
return newFile;
} catch (NumberFormatException ignored) {
}
}
var ext = target.getExtension();
return FilePath.of(target.getBaseName() + " (" + 1 + ")" + (ext.isPresent() ? "." + ext.get() : ""));
}
private void handleSingleAcrossFileSystems(FileEntry source) throws Exception {
if (target.getKind() != FileKind.DIRECTORY) {
throw new IllegalStateException("Target " + target.getPath() + " is not a directory");
}
var flatFiles = new LinkedHashMap<FileEntry, String>();
BrowserFileSystemHelper.validateDirectoryPath(target.getFileSystem(), target.getPath(), true);
var flatFiles = new LinkedHashMap<FileEntry, FilePath>();
// Prevent dropping directory into itself
if (source.getFileSystem().equals(target.getFileSystem())
@@ -302,7 +267,7 @@ public class BrowserFileTransferOperation {
}
var directoryName = source.getPath().getFileName();
flatFiles.put(source, directoryName);
flatFiles.put(source, FilePath.of(directoryName));
var baseRelative = source.getPath().getParent().toDirectory();
var list = new ArrayList<FileEntry>();
@@ -312,7 +277,7 @@ public class BrowserFileTransferOperation {
return false;
}
var rel = fileEntry.getPath().relativize(baseRelative).toUnix().toString();
var rel = fileEntry.getPath().relativize(baseRelative).toUnix();
flatFiles.put(fileEntry, rel);
if (fileEntry.getKind() == FileKind.FILE) {
// This one is up-to-date and does not need to be recalculated
@@ -329,7 +294,7 @@ public class BrowserFileTransferOperation {
return;
}
var rel = fileEntry.getPath().relativize(baseRelative).toUnix().toString();
var rel = fileEntry.getPath().relativize(baseRelative).toUnix();
flatFiles.put(fileEntry, rel);
if (fileEntry.getKind() == FileKind.FILE) {
// This one is up-to-date and does not need to be recalculated
@@ -345,7 +310,7 @@ public class BrowserFileTransferOperation {
return;
}
flatFiles.put(source, source.getPath().getFileName());
flatFiles.put(source, FilePath.of(source.getPath().getFileName()));
// If we don't have a size, it doesn't matter that much as the total size is only for display
totalSize.addAndGet(source.getFileSizeLong().orElse(0));
}
@@ -367,8 +332,7 @@ public class BrowserFileTransferOperation {
}
var sourceFile = e.getKey();
var os = targetFs.getShell().orElseThrow().getOsType();
var fixedRelPath = OsFileSystem.of(os).makeFileSystemCompatible(FilePath.of(e.getValue()));
var fixedRelPath = targetFs.makeFileSystemCompatible(e.getValue());
var targetFile = target.getPath().join(fixedRelPath.toString());
if (sourceFile.getFileSystem().equals(targetFs)) {
throw new IllegalStateException();
@@ -386,7 +350,7 @@ public class BrowserFileTransferOperation {
}
if (fileConflictChoice == BrowserDialogs.FileConflictChoice.RENAME) {
targetFile = renameFileLoop(targetFs, targetFile, false);
targetFile = BrowserFileDuplicates.renameFileDuplicate(targetFs, targetFile, false);
}
}
@@ -470,8 +434,8 @@ public class BrowserFileTransferOperation {
return;
}
sourceFs.getShell().orElseThrow().killExternal();
targetFs.getShell().orElseThrow().killExternal();
sourceFs.kill();
targetFs.kill();
};
cancelled.addListener(closeCancelListener);
@@ -545,9 +509,10 @@ public class BrowserFileTransferOperation {
outputStream.flush();
inputStream.transferTo(OutputStream.nullOutputStream());
var incomplete = readCount.get() < expectedFileSize;
var incomplete = !killStreams.get() && readCount.get() < expectedFileSize;
if (incomplete) {
throw new IOException("Source file " + sourceFile + " input did end prematurely");
throw new IOException("Source file " + sourceFile + " input size mismatch: Expected "
+ expectedFileSize + " but got " + readCount.get() + ". Did the source file get updated?");
}
} catch (Exception ex) {
exception.set(ex);
@@ -588,13 +553,7 @@ public class BrowserFileTransferOperation {
var targetFs = target.getFileSystem();
var same = files.getFirst().getFileSystem().equals(target.getFileSystem());
if (!same) {
var sourceShell = sourceFs.getShell().orElseThrow();
var targetShell = targetFs.getShell().orElseThrow();
// Check for null on shell reset
return sourceShell.getStdout() != null
&& !sourceShell.getStdout().isClosed()
&& targetShell.getStdin() != null
&& !targetShell.getStdin().isClosed();
return !sourceFs.requiresReinit() && !targetFs.requiresReinit();
} else {
return true;
}
@@ -611,20 +570,26 @@ public class BrowserFileTransferOperation {
var nowTransferred = transferred.get();
var stuck = initialTransferred == nowTransferred;
if (stuck) {
sourceFs.getShell().orElseThrow().killExternal();
targetFs.getShell().orElseThrow().killExternal();
sourceFs.kill();
targetFs.kill();
return;
}
}
}
if (!same) {
var sourceShell = sourceFs.getShell().orElseThrow();
var targetShell = targetFs.getShell().orElseThrow();
try {
sourceShell.closeStdout();
} finally {
targetShell.closeStdin();
if (sourceFs.getShell().isPresent()) {
try {
sourceFs.getShell().get().closeStdout();
} catch (Exception ignored) {
}
}
if (targetFs.getShell().isPresent()) {
try {
targetFs.getShell().get().closeStdin();
} catch (Exception ignored) {
}
}
}
}
@@ -4,7 +4,7 @@ import io.xpipe.app.comp.SimpleComp;
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.app.platform.PlatformThread;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
@@ -5,11 +5,11 @@ import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.platform.DerivedObservableList;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.DerivedObservableList;
import io.xpipe.app.util.DocumentationLink;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings;
@@ -127,13 +127,11 @@ public class BrowserHistoryTabComp extends SimpleComp {
open.setButtonGraphic(new LabelGraphic.IconGraphic("mdi2f-folder-open-outline"));
open.setButtonAction(() -> {
BrowserFullSessionModel.DEFAULT.openFileSystemAsync(
DataStorage.get().local().ref(), null, null);
DataStorage.get().local().ref(), null, null, null);
});
var v = new VerticalComp(List.of(docs, open));
v.spacing(70);
v.apply(struc -> struc.get().setAlignment(Pos.CENTER));
return v;
var list = new IntroListComp(List.of(docs, open));
return list;
}
private Comp<?> entryButton(BrowserHistorySavedState.Entry e, BooleanProperty disable) {
@@ -150,7 +148,7 @@ public class BrowserHistoryTabComp extends SimpleComp {
ThreadHelper.runAsync(() -> {
var storageEntry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
if (storageEntry.isPresent()) {
model.openFileSystemAsync(storageEntry.get().ref(), null, disable);
model.openFileSystemAsync(storageEntry.get().ref(), null, null, disable);
}
});
})
@@ -1,9 +1,9 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.ext.FileEntry;
import io.xpipe.app.ext.FileKind;
import io.xpipe.app.ext.FileSystem;
import io.xpipe.app.ext.LocalStore;
import io.xpipe.core.FileKind;
import io.xpipe.core.FilePath;
import java.nio.file.Files;
@@ -17,8 +17,8 @@ public class BrowserLocalFileSystem {
if (localFileSystem == null) {
localFileSystem = new LocalStore().createFileSystem();
localFileSystem.open();
} else if (localFileSystem.getShell().orElseThrow().isAnyStreamClosed()) {
localFileSystem.getShell().orElseThrow().restart();
} else {
localFileSystem.reinitIfNeeded();
}
}
@@ -9,9 +9,9 @@ import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.comp.base.TooltipHelper;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.platform.ContextMenuHelper;
import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ContextMenuHelper;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
@@ -1,17 +1,17 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.SimpleTitledPaneComp;
import io.xpipe.app.comp.base.VerticalComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.FileEntry;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.process.OsFileSystem;
import io.xpipe.app.process.ShellControl;
import io.xpipe.app.util.DerivedObservableList;
import io.xpipe.app.platform.DerivedObservableList;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
@@ -19,7 +19,7 @@ import javafx.scene.layout.VBox;
import lombok.SneakyThrows;
import java.util.List;
import java.util.ArrayList;
public class BrowserOverviewComp extends SimpleComp {
@@ -34,11 +34,19 @@ public class BrowserOverviewComp extends SimpleComp {
protected Region createSimple() {
// The open file system might have already been closed
ShellControl sc = model.getFileSystem().getShell().orElseThrow();
var list = new ArrayList<Comp<?>>();
var recent = DerivedObservableList.wrap(model.getSavedState().getRecentDirectories(), true)
.mapped(s -> FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory()))
.getList();
var recentOverview = new BrowserFileOverviewComp(model, recent, true);
var recentPane = new SimpleTitledPaneComp(AppI18n.observable("recent"), recentOverview, false);
recentPane.hide(Bindings.isEmpty(recent));
list.add(recentPane);
var commonPlatform = FXCollections.<FileEntry>synchronizedObservableList(FXCollections.observableArrayList());
ThreadHelper.runFailableAsync(() -> {
var common = OsFileSystem.of(sc.getOsType()).determineInterestingPaths(sc).stream()
var common = model.getFileSystem().listCommonDirectories().stream()
.map(s -> FileEntry.ofDirectory(model.getFileSystem(), s))
.filter(entry -> {
var fs = model.getFileSystem();
@@ -58,20 +66,24 @@ public class BrowserOverviewComp extends SimpleComp {
var commonOverview = new BrowserFileOverviewComp(model, commonPlatform, false);
var commonPane = new SimpleTitledPaneComp(AppI18n.observable("common"), commonOverview, false)
.apply(struc -> VBox.setVgrow(struc.get(), Priority.NEVER));
commonPane.hide(Bindings.isEmpty(commonPlatform));
list.add(commonPane);
var roots = model.getFileSystem().listRoots().stream()
.map(s -> FileEntry.ofDirectory(model.getFileSystem(), s))
.toList();
var rootsOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(roots), false);
var rootPlatform = FXCollections.<FileEntry>synchronizedObservableList(FXCollections.observableArrayList());
ThreadHelper.runFailableAsync(() -> {
var roots = model.getFileSystem().listRoots().stream()
.map(s -> FileEntry.ofDirectory(model.getFileSystem(), s))
.toList();
Platform.runLater(() -> {
rootPlatform.setAll(roots);
});
});
var rootsOverview = new BrowserFileOverviewComp(model, rootPlatform, false);
var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview, false);
rootsPane.hide(Bindings.isEmpty(rootPlatform));
list.add(rootsPane);
var recent = DerivedObservableList.wrap(model.getSavedState().getRecentDirectories(), true)
.mapped(s -> FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory()))
.getList();
var recentOverview = new BrowserFileOverviewComp(model, recent, true);
var recentPane = new SimpleTitledPaneComp(AppI18n.observable("recent"), recentOverview, false);
var vbox = new VerticalComp(List.of(recentPane, commonPane, rootsPane)).styleClass("overview");
var vbox = new VerticalComp(list).styleClass("overview");
var r = vbox.createRegion();
return r;
}
@@ -2,7 +2,7 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.IconButtonComp;
import io.xpipe.app.util.InputHelper;
import io.xpipe.app.platform.InputHelper;
import javafx.scene.layout.Region;
@@ -4,11 +4,11 @@ import io.xpipe.app.browser.icon.BrowserIconManager;
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.ext.FileKind;
import io.xpipe.app.platform.BooleanAnimationTimer;
import io.xpipe.app.platform.InputHelper;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.InputHelper;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.FileKind;
import javafx.application.Platform;
import javafx.beans.property.SimpleBooleanProperty;
@@ -8,9 +8,9 @@ import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.base.IconButtonComp;
import io.xpipe.app.comp.base.LabelComp;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.platform.BindingsHelper;
import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.util.HumanReadableFormat;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings;
@@ -100,6 +100,10 @@ public class BrowserStatusBarComp extends SimpleComp {
return null;
}
if (p.getTotal() == 0) {
return HumanReadableFormat.byteCount(p.getTransferred());
}
var elapsed = (p.getTotal() - p.getTransferred() / (double) p.getTotal()) * expected.toMillis();
var show = elapsed > 3000;
if (!show) {
@@ -4,7 +4,7 @@ import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.DerivedObservableList;
import io.xpipe.app.platform.DerivedObservableList;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings;
@@ -62,7 +62,7 @@ public class BrowserTransferComp extends SimpleComp {
return Bindings.createStringBinding(
() -> {
var p = sourceItem.get().getProgress().getValue();
if (p == null) {
if (p == null || p.getTotal() == 0) {
return entry.getFileName();
}
@@ -3,13 +3,13 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.browser.action.impl.TransferFilesActionProvider;
import io.xpipe.app.core.AppSystemInfo;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.core.mode.AppOperationMode;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.process.OsFileSystem;
import io.xpipe.app.process.ShellTemp;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.DesktopHelper;
import io.xpipe.app.util.ShellTemp;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings;
@@ -137,7 +137,7 @@ public class BrowserTransferModel {
return;
}
if (OperationMode.isInShutdown()) {
if (AppOperationMode.isInShutdown()) {
return;
}
@@ -2,9 +2,7 @@ package io.xpipe.app.browser.icon;
import io.xpipe.app.core.AppResources;
import io.xpipe.app.ext.FileEntry;
import io.xpipe.core.FileKind;
import lombok.Getter;
import io.xpipe.app.ext.FileKind;
import java.io.BufferedReader;
import java.io.InputStreamReader;
@@ -23,11 +21,6 @@ public abstract class BrowserIconDirectoryType {
public static synchronized void loadDefinitions() {
ALL.add(new BrowserIconDirectoryType() {
@Override
public String getId() {
return "root";
}
@Override
public boolean matches(FileEntry entry) {
return entry.getPath().toString().equals("/")
@@ -35,7 +28,7 @@ public abstract class BrowserIconDirectoryType {
}
@Override
public String getIcon(FileEntry entry) {
public String getIcon() {
return "browser/default_root_folder.svg";
}
});
@@ -46,7 +39,6 @@ public abstract class BrowserIconDirectoryType {
String line;
while ((line = reader.readLine()) != null) {
var split = line.split("\\|");
var id = split[0].strip();
var filter = Arrays.stream(split[1].split(","))
.map(s -> {
return s.strip();
@@ -56,7 +48,7 @@ public abstract class BrowserIconDirectoryType {
var closedIcon = "browser/" + split[2].strip();
var lightClosedIcon = split.length > 4 ? "browser/" + split[4].strip() : closedIcon;
ALL.add(new Simple(id, new BrowserIconVariant(lightClosedIcon, closedIcon), filter));
ALL.add(new Simple(new BrowserIconVariant(lightClosedIcon, closedIcon), filter));
}
}
});
@@ -66,22 +58,16 @@ public abstract class BrowserIconDirectoryType {
return ALL;
}
public abstract String getId();
public abstract boolean matches(FileEntry entry);
public abstract String getIcon(FileEntry entry);
public abstract String getIcon();
public static class Simple extends BrowserIconDirectoryType {
@Getter
private final String id;
private final BrowserIconVariant closed;
private final Set<String> names;
public Simple(String id, BrowserIconVariant closed, Set<String> names) {
this.id = id;
public Simple(BrowserIconVariant closed, Set<String> names) {
this.closed = closed;
this.names = names;
}
@@ -97,7 +83,7 @@ public abstract class BrowserIconDirectoryType {
}
@Override
public String getIcon(FileEntry entry) {
public String getIcon() {
return this.closed.getIcon();
}
}
@@ -2,7 +2,7 @@ package io.xpipe.app.browser.icon;
import io.xpipe.app.core.AppResources;
import io.xpipe.app.ext.FileEntry;
import io.xpipe.core.FileKind;
import io.xpipe.app.ext.FileKind;
import lombok.Getter;
@@ -1,7 +1,7 @@
package io.xpipe.app.browser.icon;
import io.xpipe.app.ext.FileEntry;
import io.xpipe.core.FileKind;
import io.xpipe.app.ext.FileKind;
public class BrowserIconManager {
@@ -30,7 +30,7 @@ public class BrowserIconManager {
} else {
for (var f : BrowserIconDirectoryType.getAll()) {
if (f.matches(r)) {
return f.getIcon(r);
return f.getIcon();
}
}
}
@@ -1,22 +1,27 @@
package io.xpipe.app.browser.menu;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import java.util.List;
import lombok.SneakyThrows;
public interface BrowserApplicationPathMenuProvider extends BrowserMenuItemProvider {
String getExecutable();
@Override
default void init(BrowserFileSystemTabModel model) {
default void init(BrowserFileSystemTabModel model) throws Exception {
if (model.getFileSystem().getShell().isEmpty()) {
return;
}
// Cache result for later calls
model.getCache().isApplicationInPath(getExecutable());
model.getFileSystem().getShell().get().view().isInPath(getExecutable(), true);
}
@Override
default boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return model.getCache().isApplicationInPath(getExecutable());
@SneakyThrows
default boolean isActive(BrowserFileSystemTabModel model) {
// This will always return without an exception as it is cached
return model.getFileSystem().getShell().orElseThrow().view().isInPath(getExecutable(), true);
}
}
@@ -27,11 +27,11 @@ public interface BrowserMenuBranchProvider extends BrowserMenuItemProvider {
return null;
}
var graphic = getIcon(model, selected);
var graphic = getIcon();
if (graphic != null) {
m.setGraphic(graphic.createGraphicNode());
}
m.setDisable(!isActive(model, selected));
m.setDisable(!isActive(model));
return m;
}
@@ -3,7 +3,7 @@ package io.xpipe.app.browser.menu;
import io.xpipe.app.action.ActionProvider;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.app.platform.LabelGraphic;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.MenuItem;
@@ -30,7 +30,7 @@ public interface BrowserMenuItemProvider extends ActionProvider {
: selected;
}
default LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
default LabelGraphic getIcon() {
return null;
}
@@ -52,7 +52,7 @@ public interface BrowserMenuItemProvider extends ActionProvider {
return true;
}
default boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
default boolean isActive(BrowserFileSystemTabModel model) {
return true;
}
}
@@ -75,7 +75,7 @@ public interface BrowserMenuLeafProvider extends BrowserMenuItemProvider {
});
var name = getName(model, selected);
Tooltip.install(b, TooltipHelper.create(name, getShortcut()));
var graphic = getIcon(model, selected);
var graphic = getIcon();
if (graphic != null) {
b.setGraphic(graphic.createGraphicNode());
}
@@ -88,9 +88,9 @@ public interface BrowserMenuLeafProvider extends BrowserMenuItemProvider {
}
});
b.setDisable(!isActive(model, selected));
b.setDisable(!isActive(model));
model.getCurrentPath().addListener((observable, oldValue, newValue) -> {
b.setDisable(!isActive(model, selected));
b.setDisable(!isActive(model));
});
return b;
@@ -111,12 +111,12 @@ public interface BrowserMenuLeafProvider extends BrowserMenuItemProvider {
if (getShortcut() != null) {
mi.setAccelerator(getShortcut());
}
var graphic = getIcon(model, selected);
var graphic = getIcon();
if (graphic != null) {
mi.setGraphic(graphic.createGraphicNode());
}
mi.setMnemonicParsing(false);
mi.setDisable(!isActive(model, selected));
mi.setDisable(!isActive(model));
return mi;
}
@@ -22,11 +22,13 @@ public class BrowserMenuProviders {
BrowserMenuItemProvider browserAction, BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return browserAction instanceof BrowserMenuLeafProvider
? List.of((BrowserMenuLeafProvider) browserAction)
: ((BrowserMenuBranchProvider) browserAction)
.getBranchingActions(model, entries).stream()
.map(action -> getFlattened(action, model, entries))
.flatMap(List::stream)
.toList();
: browserAction.isApplicable(model, entries)
? ((BrowserMenuBranchProvider) browserAction)
.getBranchingActions(model, entries).stream()
.map(action -> getFlattened(action, model, entries))
.flatMap(List::stream)
.toList()
: List.of();
}
public static BrowserMenuLeafProvider byId(String id, BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
@@ -4,14 +4,14 @@ 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.icon.BrowserIcons;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.app.platform.LabelGraphic;
import java.util.List;
public interface FileTypeMenuProvider extends BrowserMenuItemProvider {
@Override
default LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
default LabelGraphic getIcon() {
return new LabelGraphic.CompGraphic(BrowserIcons.createContextMenuIcon(getType()));
}
@@ -4,7 +4,8 @@ import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.value.ObservableValue;
@@ -19,7 +20,9 @@ public class BackMenuProvider implements BrowserMenuLeafProvider {
@Override
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
ThreadHelper.runAsync(() -> {
model.backSync(1);
BooleanScope.executeExclusive(model.getBusy(), () -> {
model.backSync(1);
});
});
}
@@ -33,7 +36,7 @@ public class BackMenuProvider implements BrowserMenuLeafProvider {
}
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
public LabelGraphic getIcon() {
return new LabelGraphic.IconGraphic("mdi2a-arrow-left");
}
@@ -48,7 +51,7 @@ public class BackMenuProvider implements BrowserMenuLeafProvider {
}
@Override
public boolean isActive(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
public boolean isActive(BrowserFileSystemTabModel model) {
return model.getHistory().canGoBackProperty().get();
}
}
@@ -7,6 +7,7 @@ import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.menu.BrowserMenuCategory;
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.core.OsType;
import javafx.beans.value.ObservableValue;
@@ -27,7 +28,7 @@ public class BrowseInNativeManagerMenuProvider implements BrowserMenuLeafProvide
@Override
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return switch (OsType.getLocal()) {
return switch (OsType.ofLocal()) {
case OsType.Windows ignored -> AppI18n.observable("browseInWindowsExplorer");
case OsType.Linux ignored -> AppI18n.observable("browseInDefaultFileManager");
case OsType.MacOs ignored -> AppI18n.observable("browseInFinder");
@@ -38,4 +39,9 @@ public class BrowseInNativeManagerMenuProvider implements BrowserMenuLeafProvide
public boolean acceptsEmptySelection() {
return true;
}
@Override
public LabelGraphic getIcon() {
return new LabelGraphic.IconGraphic("mdi2f-folder-eye-outline");
}
}
@@ -10,22 +10,28 @@ import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.core.FileKind;
import io.xpipe.core.OsType;
import io.xpipe.app.ext.FileKind;
import io.xpipe.app.platform.LabelGraphic;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TextField;
import lombok.SneakyThrows;
import java.util.List;
import java.util.stream.Stream;
public class ChgrpMenuProvider implements BrowserMenuBranchProvider {
@SneakyThrows
private static List<BrowserMenuItemProvider> getLeafActions(BrowserFileSystemTabModel model, boolean recursive) {
if (model.getFileSystem().getShell().isEmpty()) {
return List.of(new CustomProvider(recursive));
}
List<BrowserMenuItemProvider> actions = Stream.<BrowserMenuItemProvider>concat(
model.getCache().getGroups().entrySet().stream()
model.getFileSystem().getShell().get().view().getGroupFile().getGroups().entrySet().stream()
.filter(e -> !e.getValue().equals("nohome")
&& !e.getValue().equals("nogroup")
&& !e.getValue().equals("nobody")
@@ -38,7 +44,7 @@ public class ChgrpMenuProvider implements BrowserMenuBranchProvider {
}
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
public LabelGraphic getIcon() {
return new LabelGraphic.IconGraphic("mdi2a-account-group-outline");
}
@@ -54,8 +60,7 @@ public class ChgrpMenuProvider implements BrowserMenuBranchProvider {
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var os = model.getFileSystem().getShell().orElseThrow().getOsType();
return os != OsType.WINDOWS && os != OsType.MACOS;
return model.getFileSystem().supportsChgrp();
}
@Override
@@ -72,7 +77,7 @@ public class ChgrpMenuProvider implements BrowserMenuBranchProvider {
private static class FlatProvider implements BrowserMenuBranchProvider {
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
public LabelGraphic getIcon() {
return new LabelGraphic.IconGraphic("mdi2f-file-outline");
}
@@ -91,7 +96,7 @@ public class ChgrpMenuProvider implements BrowserMenuBranchProvider {
private static class RecursiveProvider implements BrowserMenuBranchProvider {
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
public LabelGraphic getIcon() {
return new LabelGraphic.IconGraphic("mdi2f-file-tree");
}
@@ -10,9 +10,8 @@ import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.core.FileKind;
import io.xpipe.core.OsType;
import io.xpipe.app.ext.FileKind;
import io.xpipe.app.platform.LabelGraphic;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
@@ -22,7 +21,7 @@ import java.util.List;
public class ChmodMenuProvider implements BrowserMenuBranchProvider {
private static List<BrowserMenuItemProvider> getLeafActions(BrowserFileSystemTabModel model, boolean recursive) {
private static List<BrowserMenuItemProvider> getLeafActions(BrowserFileSystemTabModel ignored, boolean recursive) {
var custom = new CustomProvider(recursive);
return List.of(
new FixedProvider("400", recursive),
@@ -37,7 +36,7 @@ public class ChmodMenuProvider implements BrowserMenuBranchProvider {
}
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
public LabelGraphic getIcon() {
return new LabelGraphic.IconGraphic("mdi2w-wrench-outline");
}
@@ -53,7 +52,7 @@ public class ChmodMenuProvider implements BrowserMenuBranchProvider {
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return model.getFileSystem().getShell().orElseThrow().getOsType() != OsType.WINDOWS;
return model.getFileSystem().supportsChmod();
}
@Override
@@ -70,7 +69,7 @@ public class ChmodMenuProvider implements BrowserMenuBranchProvider {
private static class FlatProvider implements BrowserMenuBranchProvider {
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
public LabelGraphic getIcon() {
return new LabelGraphic.IconGraphic("mdi2f-file-outline");
}
@@ -89,7 +88,7 @@ public class ChmodMenuProvider implements BrowserMenuBranchProvider {
private static class RecursiveProvider implements BrowserMenuBranchProvider {
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
public LabelGraphic getIcon() {
return new LabelGraphic.IconGraphic("mdi2f-file-tree");
}
@@ -10,22 +10,28 @@ import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.core.FileKind;
import io.xpipe.core.OsType;
import io.xpipe.app.ext.FileKind;
import io.xpipe.app.platform.LabelGraphic;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TextField;
import lombok.SneakyThrows;
import java.util.List;
import java.util.stream.Stream;
public class ChownMenuProvider implements BrowserMenuBranchProvider {
@SneakyThrows
private static List<BrowserMenuItemProvider> getLeafActions(BrowserFileSystemTabModel model, boolean recursive) {
if (model.getFileSystem().getShell().isEmpty()) {
return List.of(new CustomProvider(recursive));
}
var actions = Stream.<BrowserMenuItemProvider>concat(
model.getCache().getUsers().entrySet().stream()
model.getFileSystem().getShell().get().view().getPasswdFile().getUsers().entrySet().stream()
.filter(e -> !e.getValue().equals("nohome")
&& !e.getValue().equals("nobody")
&& (e.getKey().equals(0) || e.getKey() >= 900))
@@ -37,7 +43,7 @@ public class ChownMenuProvider implements BrowserMenuBranchProvider {
}
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
public LabelGraphic getIcon() {
return new LabelGraphic.IconGraphic("mdi2a-account-edit");
}
@@ -53,8 +59,7 @@ public class ChownMenuProvider implements BrowserMenuBranchProvider {
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
var os = model.getFileSystem().getShell().orElseThrow().getOsType();
return os != OsType.WINDOWS && os != OsType.MACOS;
return model.getFileSystem().supportsChown();
}
@Override
@@ -71,7 +76,7 @@ public class ChownMenuProvider implements BrowserMenuBranchProvider {
private static class FlatProvider implements BrowserMenuBranchProvider {
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
public LabelGraphic getIcon() {
return new LabelGraphic.IconGraphic("mdi2f-file-outline");
}
@@ -90,7 +95,7 @@ public class ChownMenuProvider implements BrowserMenuBranchProvider {
private static class RecursiveProvider implements BrowserMenuBranchProvider {
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
public LabelGraphic getIcon() {
return new LabelGraphic.IconGraphic("mdi2f-file-tree");
}
@@ -7,8 +7,8 @@ import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.menu.BrowserMenuCategory;
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.core.FileKind;
import io.xpipe.app.ext.FileKind;
import io.xpipe.app.platform.LabelGraphic;
import javafx.beans.value.ObservableValue;
@@ -21,7 +21,7 @@ public class ComputeDirectorySizesMenuProvider implements BrowserMenuLeafProvide
}
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
public LabelGraphic getIcon() {
return new LabelGraphic.IconGraphic("mdi2f-format-list-text");
}
@@ -44,8 +44,9 @@ public class ComputeDirectorySizesMenuProvider implements BrowserMenuLeafProvide
@Override
public boolean isApplicable(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return entries.stream()
.allMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY);
return model.getFileSystem().supportsDirectorySizes()
&& entries.stream()
.allMatch(browserEntry -> browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY);
}
@Override
@@ -6,7 +6,7 @@ import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.browser.menu.BrowserMenuCategory;
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.app.platform.LabelGraphic;
import javafx.beans.value.ObservableValue;
import javafx.scene.input.KeyCode;
@@ -23,7 +23,7 @@ public class CopyMenuProvider implements BrowserMenuLeafProvider {
}
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
public LabelGraphic getIcon() {
return new LabelGraphic.IconGraphic("mdoal-file_copy");
}
@@ -41,9 +41,4 @@ public class CopyMenuProvider implements BrowserMenuLeafProvider {
public ObservableValue<String> getName(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return AppI18n.observable("copy");
}
@Override
public boolean acceptsEmptySelection() {
return true;
}
}
@@ -6,9 +6,9 @@ import io.xpipe.app.browser.menu.BrowserMenuBranchProvider;
import io.xpipe.app.browser.menu.BrowserMenuCategory;
import io.xpipe.app.browser.menu.BrowserMenuLeafProvider;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.ClipboardHelper;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.core.FileKind;
import io.xpipe.app.ext.FileKind;
import io.xpipe.app.platform.ClipboardHelper;
import io.xpipe.app.platform.LabelGraphic;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
@@ -35,7 +35,7 @@ public class CopyPathMenuProvider implements BrowserMenuBranchProvider {
}
@Override
public LabelGraphic getIcon(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
public LabelGraphic getIcon() {
return new LabelGraphic.IconGraphic("mdi2c-content-copy");
}

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