diff --git a/.gitignore b/.gitignore
index 28b79aa8b..596a2e7c9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,8 @@
.gradle/
build/
-.idea
+.idea/*
+!.idea/codeStyles
+!.idea/inspectionProfiles
lib/
dev.properties
extensions.txt
diff --git a/README.md b/README.md
index d58c36535..932b39cda 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,6 @@
-### A brand-new shell connection hub and remote file manager
-
-XPipe is a new type of shell connection hub and remote file manager that allows you to access your entire server infrastructure from your local machine. It works on top of your installed command-line programs that you normally use to connect and does not require any setup on your remote systems.
+XPipe is a new type of shell connection hub and remote file manager that allows you to access your entire server infrastructure from your local machine. It works on top of your installed command-line programs and does not require any setup on your remote systems.
XPipe fully integrates with your tools such as your favourite text/code editors, terminals, shells, command-line tools and more. The platform is designed to be extensible, allowing anyone to add easily support for more tools or to implement custom functionality through a modular extension system.
@@ -17,21 +15,22 @@ It currently supports:
## Connection Hub
- Easily connect to and access all kinds of remote connections in one place
-- Securely stores all information exclusively on your computer and encrypts all secret information. See the [security page](/SECURITY.md) for more information
+- Securely stores all information exclusively on your computer and encrypts all secret information
- Allows you to create specific login environments on any system to instantly jump into a properly set up environment for every use case
- Can create desktop shortcuts that automatically open remote connections in your terminal
- Group all your connections into hierarchical categories
-![connections](https://github.com/xpipe-io/xpipe/assets/72509152/ef19aa85-1b66-45e0-a051-5a4658758626)
+![connections](https://github.com/xpipe-io/xpipe/assets/72509152/3a690fe3-29b8-43fc-a1d1-1dee9be71d4d)
## Remote File Manager
- Interact with the file system of any remote system using a workflow optimized for professionals
-- Quickly open a terminal into any directory
+- Quickly open a terminal session into any directory in your favourite terminal emulator
- Utilize your favourite local programs to open and edit remote files
-- Dynamically elevate sessions with sudo when required
+- Dynamically elevate sessions with sudo when required without having to restart the session
+- Integrates with your local desktop environment for a seamless transfer of local files
-![browser](https://github.com/xpipe-io/xpipe/assets/72509152/5631fe50-58b4-4847-a5f4-ad3898a02a9f)
+![browser](https://github.com/xpipe-io/xpipe/assets/72509152/60d70293-c513-4f12-b242-30610ce5ab5d)
## Terminal Launcher
@@ -44,13 +43,22 @@ It currently supports:
-
+
-## Downloads
+## Versatile scripting system
-Note that this is a desktop application that should be run on your local desktop workstation, not on any server or containers. It will be able to connect to your server infrastructure with ease from your local machine.
+- Create reusable simple shell scripts, templates, and groups to run on connected remote systems
+- Automatically make your scripts available in the PATH on any remote system without any setup
+- Setup shell init environments for connections to fully customize your work environment for every purpose
+- Open custom shells and custom remote connections by providing your own commands
+
+![scripts](https://github.com/xpipe-io/xpipe/assets/72509152/2d473f7b-ae1d-4dd1-86a3-02658b094da5)
+
+# Downloads
+
+Note that this is a desktop application that should be run on your local desktop workstation, not on any server or containers. It will be able to connect to your server infrastructure from there.
### Installers
@@ -94,7 +102,6 @@ The script supports installation via `apt`, `rpm`, and `pacman` on Linux, plus a
bash <(curl -sL https://raw.githubusercontent.com/xpipe-io/xpipe/master/get-xpipe.sh)
```
-
### Package managers
Alternatively, you can also use your favorite package manager (if supported):
@@ -107,7 +114,7 @@ Alternatively, you can also use your favorite package manager (if supported):
XPipe utilizes an open core model, which essentially means that the main application is open source while certain other components are not. Select parts are not open source yet, but may be added to this repository in the future.
-This mainly concerns the features only available in the professional tier and the shell handling library implementation and extensions for configuring and handling shell connections. Furthermore, some tests and especially test environments and that run on private servers are also not included in this repository. Finally, scripts and workflows to create and publish installers and packages are also not included to prevent attackers from easily impersonating the XPipe application.
+This mainly concerns the features only available in the professional tier and the shell handling library implementation. Furthermore, some tests and especially test environments and that run on private servers are also not included in this repository.
## Further information
diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkList.java b/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkList.java
index 40648e373..6bec4c8ae 100644
--- a/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkList.java
+++ b/app/src/main/java/io/xpipe/app/browser/BrowserBookmarkList.java
@@ -1,7 +1,10 @@
package io.xpipe.app.browser;
import atlantafx.base.theme.Styles;
-import io.xpipe.app.comp.store.*;
+import io.xpipe.app.comp.store.StoreEntryWrapper;
+import io.xpipe.app.comp.store.StoreSection;
+import io.xpipe.app.comp.store.StoreSectionMiniComp;
+import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FilterComp;
@@ -31,14 +34,12 @@ import java.util.function.Predicate;
final class BrowserBookmarkList extends SimpleComp {
- private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
-
public static final Timer DROP_TIMER = new Timer("dnd", true);
+ private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
+ private final BrowserModel model;
private Point2D lastOver = new Point2D(-1, -1);
private TimerTask activeTask;
- private final BrowserModel model;
-
BrowserBookmarkList(BrowserModel model) {
this.model = model;
}
@@ -48,28 +49,20 @@ final class BrowserBookmarkList extends SimpleComp {
var filterText = new SimpleStringProperty();
var open = PlatformThread.sync(model.getSelected());
Predicate applicable = storeEntryWrapper -> {
- return (storeEntryWrapper.getEntry().getStore() instanceof ShellStore
- || storeEntryWrapper.getEntry().getStore() instanceof FixedHierarchyStore)
- && storeEntryWrapper.getEntry().getValidity().isUsable();
- };
+ return (storeEntryWrapper.getEntry().getStore() instanceof ShellStore ||
+ storeEntryWrapper.getEntry().getStore() instanceof FixedHierarchyStore) && storeEntryWrapper.getEntry().getValidity().isUsable();
+ };
var selectedCategory = new SimpleObjectProperty<>(StoreViewState.get().getActiveCategory().getValue());
var section = StoreSectionMiniComp.createList(
- StoreSection.createTopLevel(
- StoreViewState.get().getAllEntries(), applicable, filterText, selectedCategory),
- (s, comp) -> {
+ StoreSection.createTopLevel(StoreViewState.get().getAllEntries(), applicable, filterText, selectedCategory), (s, comp) -> {
BooleanProperty busy = new SimpleBooleanProperty(false);
comp.disable(Bindings.createBooleanBinding(() -> {
return busy.get() || !applicable.test(s.getWrapper());
}, busy));
comp.apply(struc -> {
open.addListener((observable, oldValue, newValue) -> {
- struc.get()
- .pseudoClassStateChanged(
- SELECTED,
- newValue != null
- && newValue.getEntry().get()
- .equals(s.getWrapper()
- .getEntry()));
+ struc.get().pseudoClassStateChanged(SELECTED,
+ newValue != null && newValue.getEntry().get().equals(s.getWrapper().getEntry()));
});
struc.get().setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
@@ -90,19 +83,14 @@ final class BrowserBookmarkList extends SimpleComp {
});
});
});
- var category = new DataStoreCategoryChoiceComp(StoreViewState.get().getAllConnectionsCategory(), StoreViewState.get().getActiveCategory(), selectedCategory)
- .styleClass(Styles.LEFT_PILL)
- .grow(false, true);
- var filter =
- new FilterComp(filterText).styleClass(Styles.RIGHT_PILL).hgrow().apply(struc -> {});
+ var category = new DataStoreCategoryChoiceComp(StoreViewState.get().getAllConnectionsCategory(), StoreViewState.get().getActiveCategory(),
+ selectedCategory).styleClass(Styles.LEFT_PILL).grow(false, true);
+ var filter = new FilterComp(filterText).styleClass(Styles.RIGHT_PILL).hgrow().apply(struc -> {});
- var top = new HorizontalComp(List.of(category, filter.hgrow()))
- .styleClass("categories")
- .apply(struc -> {
- AppFont.medium(struc.get());
- struc.get().setFillHeight(true);
- })
- .createRegion();
+ var top = new HorizontalComp(List.of(category, filter.hgrow())).styleClass("categories").apply(struc -> {
+ AppFont.medium(struc.get());
+ struc.get().setFillHeight(true);
+ }).createRegion();
var r = section.vgrow().createRegion();
var content = new VBox(top, r);
content.setFillWidth(true);
diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserComp.java
index 6feeba6ec..a9ad1b1ea 100644
--- a/app/src/main/java/io/xpipe/app/browser/BrowserComp.java
+++ b/app/src/main/java/io/xpipe/app/browser/BrowserComp.java
@@ -170,30 +170,45 @@ public class BrowserComp extends SimpleComp {
var modifying = new SimpleBooleanProperty();
// Handle selection from platform
- tabs.getSelectionModel().selectedIndexProperty().addListener((observable, oldValue, newValue) -> {
+ tabs.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
if (modifying.get()) {
return;
}
- if (newValue.intValue() == -1) {
+ if (newValue == null) {
model.getSelected().setValue(null);
return;
}
- model.getSelected().setValue(model.getOpenFileSystems().get(newValue.intValue()));
+ var source = map.entrySet().stream()
+ .filter(openFileSystemModelTabEntry ->
+ openFileSystemModelTabEntry.getValue().equals(newValue))
+ .findAny()
+ .map(Map.Entry::getKey)
+ .orElse(null);
+ model.getSelected().setValue(source);
});
// Handle selection from model
model.getSelected().addListener((observable, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
- var index = model.getOpenFileSystems().indexOf(newValue);
- if (index == -1 || index >= tabs.getTabs().size()) {
+ if (newValue == null) {
tabs.getSelectionModel().select(null);
return;
}
- var tab = tabs.getTabs().get(index);
- tabs.getSelectionModel().select(tab);
+ var toSelect = map.entrySet().stream()
+ .filter(openFileSystemModelTabEntry ->
+ openFileSystemModelTabEntry.getKey().equals(newValue))
+ .findAny()
+ .map(Map.Entry::getValue)
+ .orElse(null);
+ if (toSelect == null || !tabs.getTabs().contains(toSelect)) {
+ tabs.getSelectionModel().select(null);
+ return;
+ }
+
+ tabs.getSelectionModel().select(toSelect);
});
});
diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserModel.java b/app/src/main/java/io/xpipe/app/browser/BrowserModel.java
index b0fc483ab..7cd88c042 100644
--- a/app/src/main/java/io/xpipe/app/browser/BrowserModel.java
+++ b/app/src/main/java/io/xpipe/app/browser/BrowserModel.java
@@ -24,6 +24,15 @@ import java.util.function.Consumer;
@Getter
public class BrowserModel {
+ public static final BrowserModel DEFAULT = new BrowserModel(Mode.BROWSER);
+ private final Mode mode;
+ private final ObservableList openFileSystems = FXCollections.observableArrayList();
+ private final Property selected = new SimpleObjectProperty<>();
+ private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this);
+ private final ObservableList selection = FXCollections.observableArrayList();
+ @Setter
+ private Consumer> onFinish;
+
public BrowserModel(Mode mode) {
this.mode = mode;
@@ -36,40 +45,6 @@ public class BrowserModel {
});
}
- @Getter
- public enum Mode {
- BROWSER(false, true, true, true),
- SINGLE_FILE_CHOOSER(true, false, true, false),
- SINGLE_FILE_SAVE(true, false, true, false),
- MULTIPLE_FILE_CHOOSER(true, true, true, false),
- SINGLE_DIRECTORY_CHOOSER(true, false, false, true),
- MULTIPLE_DIRECTORY_CHOOSER(true, true, false, true);
-
- private final boolean chooser;
- private final boolean multiple;
- private final boolean acceptsFiles;
- private final boolean acceptsDirectories;
-
- Mode(boolean chooser, boolean multiple, boolean acceptsFiles, boolean acceptsDirectories) {
- this.chooser = chooser;
- this.multiple = multiple;
- this.acceptsFiles = acceptsFiles;
- this.acceptsDirectories = acceptsDirectories;
- }
- }
-
- public static final BrowserModel DEFAULT = new BrowserModel(Mode.BROWSER);
-
- private final Mode mode;
-
- @Setter
- private Consumer> onFinish;
-
- private final ObservableList openFileSystems = FXCollections.observableArrayList();
- private final Property selected = new SimpleObjectProperty<>();
- private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this);
- private final ObservableList selection = FXCollections.observableArrayList();
-
public void restoreState(BrowserSavedState state) {
state.getLastSystems().forEach(e -> {
restoreState(e, null);
@@ -87,8 +62,7 @@ public class BrowserModel {
var list = new ArrayList();
openFileSystems.forEach(model -> {
if (DataStorage.get().getStoreEntries().contains(model.getEntry().get())) {
- list.add(new BrowserSavedState.Entry(
- model.getEntry().get().getUuid(), model.getCurrentPath().get()));
+ list.add(new BrowserSavedState.Entry(model.getEntry().get().getUuid(), model.getCurrentPath().get()));
}
});
@@ -120,11 +94,8 @@ public class BrowserModel {
return;
}
- var stores = chosen.stream()
- .map(entry -> new FileStore(
- entry.getRawFileEntry().getFileSystem().getStore(),
- entry.getRawFileEntry().getPath()))
- .toList();
+ var stores = chosen.stream().map(
+ entry -> new FileStore(entry.getRawFileEntry().getFileSystem().getStore(), entry.getRawFileEntry().getPath())).toList();
onFinish.accept(stores);
}
@@ -140,10 +111,8 @@ public class BrowserModel {
});
}
- public void openExistingFileSystemIfPresent(DataStoreEntryRef extends FileSystemStore> store) {
- var found = openFileSystems.stream()
- .filter(model -> Objects.equals(model.getEntry(), store))
- .findFirst();
+ public void openExistingFileSystemIfPresent(DataStoreEntryRef extends FileSystemStore> store) {
+ var found = openFileSystems.stream().filter(model -> Objects.equals(model.getEntry(), store)).findFirst();
if (found.isPresent()) {
selected.setValue(found.get());
} else {
@@ -176,9 +145,9 @@ public class BrowserModel {
ThreadHelper.runFailableAsync(() -> {
OpenFileSystemModel model;
- // Prevent multiple calls from interfering with each other
- synchronized (BrowserModel.this) {
- try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
+ try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
+ // Prevent multiple calls from interfering with each other
+ synchronized (BrowserModel.this) {
model = new OpenFileSystemModel(this, store);
model.initFileSystem();
model.initSavedState();
@@ -194,4 +163,26 @@ public class BrowserModel {
}
});
}
+
+ @Getter
+ public enum Mode {
+ BROWSER(false, true, true, true),
+ SINGLE_FILE_CHOOSER(true, false, true, false),
+ SINGLE_FILE_SAVE(true, false, true, false),
+ MULTIPLE_FILE_CHOOSER(true, true, true, false),
+ SINGLE_DIRECTORY_CHOOSER(true, false, false, true),
+ MULTIPLE_DIRECTORY_CHOOSER(true, true, false, true);
+
+ private final boolean chooser;
+ private final boolean multiple;
+ private final boolean acceptsFiles;
+ private final boolean acceptsDirectories;
+
+ Mode(boolean chooser, boolean multiple, boolean acceptsFiles, boolean acceptsDirectories) {
+ this.chooser = chooser;
+ this.multiple = multiple;
+ this.acceptsFiles = acceptsFiles;
+ this.acceptsDirectories = acceptsDirectories;
+ }
+ }
}
diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java
index 1cd9efe75..d3102358d 100644
--- a/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java
+++ b/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java
@@ -1,20 +1,25 @@
package io.xpipe.app.browser;
import atlantafx.base.controls.Spacer;
-import atlantafx.base.controls.Tile;
import atlantafx.base.theme.Styles;
+import io.xpipe.app.comp.base.ButtonComp;
+import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.comp.base.TileButtonComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.storage.DataStorage;
+import io.xpipe.app.util.JfxHelper;
+import io.xpipe.app.util.ThreadHelper;
+import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
+import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.control.Label;
-import javafx.scene.control.ScrollPane;
import javafx.scene.control.Separator;
import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
@@ -53,40 +58,45 @@ public class BrowserWelcomeComp extends SimpleComp {
var storeList = new VBox();
storeList.setSpacing(8);
- state.getLastSystems().forEach(e -> {
+
+ var list = FXCollections.observableList(state.getLastSystems().stream().filter(e -> {
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
if (entry.isEmpty()) {
- return;
+ return false;
}
if (!entry.get().getValidity().isUsable()) {
- return;
+ return false;
}
- var graphic =
- entry.get().getProvider().getDisplayIconFileName(entry.get().getStore());
+ return true;
+ }).toList());
+ var box = new ListBoxViewComp<>(list, list, e -> {
+ var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
+ var graphic = entry.get().getProvider().getDisplayIconFileName(entry.get().getStore());
var view = PrettyImageHelper.ofFixedSquare(graphic, 45);
view.padding(new Insets(2, 8, 2, 8));
- var tile = new Tile(
- DataStorage.get().getStoreDisplayName(entry.get()),
- e.getPath(),
- view.createRegion());
- tile.setActionHandler(() -> {
- model.restoreState(e, tile.disableProperty());
- });
- storeList.getChildren().add(tile);
- });
-
- var sp = new ScrollPane(storeList);
- sp.setFitToWidth(true);
+ var content =
+ JfxHelper.createNamedEntry(DataStorage.get().getStoreDisplayName(entry.get()), e.getPath(), graphic);
+ var disable = new SimpleBooleanProperty();
+ return new ButtonComp(null, content, () -> {
+ ThreadHelper.runAsync(() -> {
+ model.restoreState(e, disable);
+ });
+ }).disable(disable).styleClass("color-box").apply(struc -> struc.get().setMaxWidth(2000)).grow(true, false);
+ }).apply(struc -> {
+ VBox vBox = (VBox) struc.get().getContent();
+ vBox.setSpacing(10);
+ }).createRegion();
var layout = new VBox();
- layout.setMaxWidth(700);
+ layout.getStyleClass().add("welcome");
layout.setPadding(new Insets(40, 40, 40, 50));
layout.setSpacing(18);
layout.getChildren().add(hbox);
layout.getChildren().add(new Separator(Orientation.HORIZONTAL));
- layout.getChildren().add(sp);
+ layout.getChildren().add(box);
+ VBox.setVgrow(layout.getChildren().get(2), Priority.NEVER);
layout.getChildren().add(new Separator(Orientation.HORIZONTAL));
var tile = new TileButtonComp("restore", "restoreAllSessions", "mdmz-restore", actionEvent -> {
diff --git a/app/src/main/java/io/xpipe/app/browser/FileSystemHelper.java b/app/src/main/java/io/xpipe/app/browser/FileSystemHelper.java
index ac39454c3..34182882d 100644
--- a/app/src/main/java/io/xpipe/app/browser/FileSystemHelper.java
+++ b/app/src/main/java/io/xpipe/app/browser/FileSystemHelper.java
@@ -124,7 +124,7 @@ public class FileSystemHelper {
}
if (!model.getFileSystem().directoryExists(path)) {
- throw new IllegalArgumentException(String.format("Directory %s does not exist", path));
+ throw ErrorEvent.unreportable(new IllegalArgumentException(String.format("Directory %s does not exist", path)));
}
model.getFileSystem().directoryAccessible(path);
diff --git a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java
index a146a395b..99619e756 100644
--- a/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java
+++ b/app/src/main/java/io/xpipe/app/browser/OpenFileSystemModel.java
@@ -7,9 +7,7 @@ import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.TerminalHelper;
import io.xpipe.app.util.ThreadHelper;
-import io.xpipe.core.process.ProcessControlProvider;
-import io.xpipe.core.process.ShellControl;
-import io.xpipe.core.process.ShellDialects;
+import io.xpipe.core.process.*;
import io.xpipe.core.store.*;
import io.xpipe.core.util.FailableConsumer;
import javafx.beans.binding.Bindings;
@@ -155,21 +153,25 @@ public final class OpenFileSystemModel {
var name = adjustedPath + " - " + entry.get().getName();
ThreadHelper.runFailableAsync(() -> {
if (ShellDialects.ALL.stream().anyMatch(dialect -> adjustedPath.startsWith(dialect.getOpenCommand()))) {
- TerminalHelper.open(entry.getEntry(), name, fileSystem
- .getShell()
- .get()
- .subShell(adjustedPath)
- .initWith(fileSystem
- .getShell()
- .get()
- .getShellDialect()
- .getCdCommand(currentPath.get())));
+ TerminalHelper.open(
+ entry.getEntry(),
+ name,
+ fileSystem
+ .getShell()
+ .get()
+ .subShell(adjustedPath)
+ .initWith(new SimpleScriptSnippet(
+ fileSystem
+ .getShell()
+ .get()
+ .getShellDialect()
+ .getCdCommand(currentPath.get()),
+ ScriptSnippet.ExecutionType.BOTH)));
} else {
- TerminalHelper.open(entry.getEntry(), name, fileSystem
- .getShell()
- .get()
- .command(adjustedPath)
- .withWorkingDirectory(directory));
+ TerminalHelper.open(
+ entry.getEntry(),
+ name,
+ fileSystem.getShell().get().command(adjustedPath).withWorkingDirectory(directory));
}
});
return Optional.ofNullable(currentPath.get());
@@ -363,8 +365,9 @@ public final class OpenFileSystemModel {
var fs = entry.getStore().createFileSystem();
fs.open();
this.fileSystem = fs;
- this.local =
- fs.getShell().map(shellControl -> shellControl.hasLocalSystemAccess()).orElse(false);
+ this.local = fs.getShell()
+ .map(shellControl -> shellControl.hasLocalSystemAccess())
+ .orElse(false);
this.cache.init();
});
}
@@ -393,7 +396,10 @@ public final class OpenFileSystemModel {
var connection = ((ConnectionFileSystem) fileSystem).getShellControl();
var name = directory + " - " + entry.get().getName();
var toOpen = ProcessControlProvider.get().withDefaultScripts(connection);
- TerminalHelper.open(entry.getEntry(), name, toOpen.initWith(connection.getShellDialect().getCdCommand(directory)));
+ TerminalHelper.open(
+ entry.getEntry(),
+ name,
+ toOpen.initWith(new SimpleScriptSnippet(connection.getShellDialect().getCdCommand(directory), ScriptSnippet.ExecutionType.BOTH)));
// Restart connection as we will have to start it anyway, so we speed it up by doing it preemptively
connection.start();
diff --git a/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java b/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java
index 7715b9ddc..90df02c42 100644
--- a/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/base/SideMenuBarComp.java
@@ -80,14 +80,14 @@ public class SideMenuBarComp extends Comp> {
vbox.getChildren().add(b.createRegion());
}
- {
- var b = new IconButtonComp("mdi2c-comment-processing-outline", () -> Hyperlinks.open(Hyperlinks.ROADMAP))
- .apply(new FancyTooltipAugment<>("roadmap"));
- b.apply(struc -> {
- AppFont.setSize(struc.get(), 2);
- });
- vbox.getChildren().add(b.createRegion());
- }
+// {
+// var b = new IconButtonComp("mdi2c-comment-processing-outline", () -> Hyperlinks.open(Hyperlinks.ROADMAP))
+// .apply(new FancyTooltipAugment<>("roadmap"));
+// b.apply(struc -> {
+// AppFont.setSize(struc.get(), 2);
+// });
+// vbox.getChildren().add(b.createRegion());
+// }
{
diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java
index 6c2feef2a..c89aa19cd 100644
--- a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java
@@ -345,7 +345,7 @@ public abstract class StoreEntryComp extends SimpleComp {
if (wrapper.getEntry().getProvider() != null && wrapper.getEntry().getProvider().canMoveCategories()) {
var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline"));
- StoreViewState.get().getSortedCategories(wrapper.getCategory().getValue()).forEach(storeCategoryWrapper -> {
+ StoreViewState.get().getSortedCategories(wrapper.getCategory().getValue().getRoot()).forEach(storeCategoryWrapper -> {
MenuItem m = new MenuItem(storeCategoryWrapper.getName());
m.setOnAction(event -> {
wrapper.moveTo(storeCategoryWrapper.getCategory());
diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java b/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java
index 29cf83f14..15c95390a 100644
--- a/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java
+++ b/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java
@@ -46,7 +46,7 @@ public class StoreSection {
if (wrapper != null) {
this.showDetails = Bindings.createBooleanBinding(
() -> {
- return wrapper.getExpanded().get() || allChildren.size() == 0;
+ return wrapper.getExpanded().get() || allChildren.isEmpty();
},
wrapper.getExpanded(),
allChildren);
@@ -63,20 +63,19 @@ public class StoreSection {
var c = Comparator.comparingInt(
value -> value.getWrapper().getEntry().getValidity().isUsable() ? -1 : 1);
- var mapped = BindingsHelper.mappedBinding(category, storeCategoryWrapper -> storeCategoryWrapper.getSortMode());
+ var mappedSortMode = BindingsHelper.mappedBinding(category, storeCategoryWrapper -> storeCategoryWrapper != null ? storeCategoryWrapper.getSortMode() : null);
return BindingsHelper.orderedContentBinding(
list,
(o1, o2) -> {
- var current = category.getValue();
+ var current = mappedSortMode.getValue();
if (current != null) {
- return c.thenComparing(current.getSortMode().getValue().comparator())
+ return c.thenComparing(current.comparator())
.compare(o1, o2);
} else {
return c.compare(o1, o2);
}
},
- category,
- mapped);
+ mappedSortMode);
}
public static StoreSection createTopLevel(
@@ -118,10 +117,12 @@ public class StoreSection {
}
var allChildren = BindingsHelper.filteredContentBinding(all, other -> {
+ // Legacy implementation that does not use caches. Use for testing
// if (true) return DataStorage.get()
// .getDisplayParent(other.getEntry())
// .map(found -> found.equals(e.getEntry()))
// .orElse(false);
+
// This check is fast as the children are cached in the storage
return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry());
});
@@ -131,8 +132,12 @@ public class StoreSection {
var filtered = BindingsHelper.filteredContentBinding(
ordered,
section -> {
- return (filterString == null || section.shouldShow(filterString.get()))
- && section.anyMatches(entryFilter);
+ var showFilter = filterString == null || section.shouldShow(filterString.get());
+ var matchesSelector = section.anyMatches(entryFilter);
+ var sameCategory = category == null || category.getValue() == null || category.getValue().contains(section.getWrapper().getEntry());
+ // If this entry is already shown as root due to a different category than parent, don't show it again here
+ var notRoot = !DataStorage.get().isRootEntry(section.getWrapper().getEntry());
+ return showFilter && matchesSelector && sameCategory && notRoot;
},
category,
filterString);
diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java
index 464048e20..fd030ad05 100644
--- a/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java
+++ b/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java
@@ -57,6 +57,7 @@ public class StoreSectionMiniComp extends Comp> {
struc.get().setAlignment(Pos.CENTER_LEFT);
})
.grow(true, false)
+ .apply(struc -> struc.get().setMnemonicParsing(false))
.styleClass("item");
augment.accept(section, root);
@@ -81,7 +82,6 @@ public class StoreSectionMiniComp extends Comp> {
List> topEntryList = List.of(button, root);
list.add(new HorizontalComp(topEntryList)
.apply(struc -> struc.get().setFillHeight(true)));
- list.add(Comp.separator().visible(expanded));
} else {
expanded = new SimpleBooleanProperty(true);
}
diff --git a/app/src/main/java/io/xpipe/app/core/App.java b/app/src/main/java/io/xpipe/app/core/App.java
index 597b0f377..7856bc2f8 100644
--- a/app/src/main/java/io/xpipe/app/core/App.java
+++ b/app/src/main/java/io/xpipe/app/core/App.java
@@ -37,7 +37,7 @@ public class App extends Application {
// This is necessary in case XPipe was started through a script as it will have no icon otherwise
if (OsType.getLocal().equals(OsType.MACOS) && AppProperties.get().isDeveloperMode() && AppLogs.get().isWriteToSysout()) {
try {
- var iconUrl = Main.class.getResourceAsStream("resources/img/logo/logo_128x128.png");
+ var iconUrl = Main.class.getResourceAsStream("resources/img/logo/logo_macos_128x128.png");
if (iconUrl != null) {
var awtIcon = ImageIO.read(iconUrl);
Taskbar.getTaskbar().setIconImage(awtIcon);
diff --git a/app/src/main/java/io/xpipe/app/core/AppMainWindow.java b/app/src/main/java/io/xpipe/app/core/AppMainWindow.java
index 1c35d14ec..26562960c 100644
--- a/app/src/main/java/io/xpipe/app/core/AppMainWindow.java
+++ b/app/src/main/java/io/xpipe/app/core/AppMainWindow.java
@@ -174,8 +174,8 @@ public class AppMainWindow {
stage.setWidth(1280);
stage.setHeight(720);
} else {
- stage.setX(310);
- stage.setY(178);
+ stage.setX(312);
+ stage.setY(149);
stage.setWidth(1296);
stage.setHeight(759);
}
diff --git a/app/src/main/java/io/xpipe/app/core/AppTheme.java b/app/src/main/java/io/xpipe/app/core/AppTheme.java
index ba5f094ef..5cdc08974 100644
--- a/app/src/main/java/io/xpipe/app/core/AppTheme.java
+++ b/app/src/main/java/io/xpipe/app/core/AppTheme.java
@@ -200,7 +200,7 @@ public class AppTheme {
static Theme getDefaultLightTheme() {
return switch (OsType.getLocal()) {
case OsType.Windows windows -> PRIMER_LIGHT;
- case OsType.Linux linux -> NORD_LIGHT;
+ case OsType.Linux linux -> PRIMER_LIGHT;
case OsType.MacOs macOs -> CUPERTINO_LIGHT;
};
}
@@ -208,7 +208,7 @@ public class AppTheme {
static Theme getDefaultDarkTheme() {
return switch (OsType.getLocal()) {
case OsType.Windows windows -> PRIMER_DARK;
- case OsType.Linux linux -> NORD_DARK;
+ case OsType.Linux linux -> PRIMER_DARK;
case OsType.MacOs macOs -> CUPERTINO_DARK;
};
}
diff --git a/app/src/main/java/io/xpipe/app/core/AppTrayIcon.java b/app/src/main/java/io/xpipe/app/core/AppTrayIcon.java
index 9b8e3f0b5..f565ffb4b 100644
--- a/app/src/main/java/io/xpipe/app/core/AppTrayIcon.java
+++ b/app/src/main/java/io/xpipe/app/core/AppTrayIcon.java
@@ -28,7 +28,7 @@ public class AppTrayIcon {
var image = switch (OsType.getLocal()) {
case OsType.Windows windows -> "img/logo/logo_16x16.png";
case OsType.Linux linux -> "img/logo/logo_24x24.png";
- case OsType.MacOs macOs -> "img/logo/logo_24x24.png";
+ case OsType.MacOs macOs -> "img/logo/logo_macos_tray_24x24.png";
};
var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, image).orElseThrow();
diff --git a/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java b/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java
index edf743b51..3110b903c 100644
--- a/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java
+++ b/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java
@@ -69,6 +69,7 @@ public class BaseMode extends OperationMode {
DataStorage.reset();
AppPrefs.reset();
AppExtensionManager.reset();
+ AppDataLock.unlock();
// Shut down socket server last to keep a non-daemon thread running
AppSocketServer.reset();
TrackEvent.info("mode", "Background mode shutdown finished");
diff --git a/app/src/main/java/io/xpipe/app/ext/ExtensionException.java b/app/src/main/java/io/xpipe/app/ext/ExtensionException.java
index c1fe5abc0..bd7b0c238 100644
--- a/app/src/main/java/io/xpipe/app/ext/ExtensionException.java
+++ b/app/src/main/java/io/xpipe/app/ext/ExtensionException.java
@@ -19,4 +19,8 @@ public class ExtensionException extends RuntimeException {
public ExtensionException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
+
+ public static ExtensionException corrupt(String message) {
+ return new ExtensionException(message + ". Is the installation corrupt?");
+ }
}
diff --git a/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java b/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java
index 9ba7c42be..698c32eeb 100644
--- a/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java
+++ b/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java
@@ -9,6 +9,7 @@ import io.xpipe.app.core.AppState;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.update.XPipeDistributionType;
+import io.xpipe.app.util.LicenseProvider;
import org.apache.commons.io.FileUtils;
import java.io.*;
@@ -150,6 +151,7 @@ public class SentryErrorHandler implements ErrorHandler {
atts.forEach(attachment -> s.addAttachment(attachment));
}
+ s.setTag("hasLicense", String.valueOf(LicenseProvider.get().hasLicense()));
s.setTag("updatesEnabled", AppPrefs.get() != null ? AppPrefs.get().automaticallyUpdate().getValue().toString() : "unknown");
s.setTag("initError", String.valueOf(OperationMode.isInStartup()));
s.setTag(
diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java
index 9daaed86e..2fbb5a21a 100644
--- a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java
+++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java
@@ -359,6 +359,9 @@ public class AppPrefs {
private AppPreferencesFx preferencesFx;
private boolean controlsSetup;
+ @Getter
+ private final Set> proRequiredSettings = new HashSet<>();
+
private AppPrefs() {
try {
preferencesFx = createPreferences();
diff --git a/app/src/main/java/io/xpipe/app/prefs/CustomFormRenderer.java b/app/src/main/java/io/xpipe/app/prefs/CustomFormRenderer.java
index 21736e2cc..c39072053 100644
--- a/app/src/main/java/io/xpipe/app/prefs/CustomFormRenderer.java
+++ b/app/src/main/java/io/xpipe/app/prefs/CustomFormRenderer.java
@@ -79,7 +79,7 @@ public class CustomFormRenderer extends PreferencesFxFormRenderer {
c.getFieldLabel().setMaxHeight(AppFont.getPixelSize(1));
c.getFieldLabel().textProperty().unbind();
c.getFieldLabel().textProperty().bind(Bindings.createStringBinding(() -> {
- return f.labelProperty().get() + (f.isEditable() ? "" : " (Pro)");
+ return f.labelProperty().get() + (AppPrefs.get().getProRequiredSettings().contains(f) ? " (Pro)" : "");
}, f.labelProperty()));
grid.add(c.getFieldLabel(), 0, i + rowAmount);
diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java
index bfa0932b7..a7d91eff1 100644
--- a/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java
+++ b/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java
@@ -177,13 +177,21 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
@Override
protected Optional determineInstallation() {
- Optional launcherDir;
- launcherDir = WindowsRegistry.readString(
+ var perUser = WindowsRegistry.readString(
WindowsRegistry.HKEY_CURRENT_USER,
"SOFTWARE\\71445fac-d6ef-5436-9da7-5a323762d7f5",
"InstallLocation")
- .map(p -> p + "\\Tabby.exe");
- return launcherDir.map(Path::of);
+ .map(p -> p + "\\Tabby.exe").map(Path::of);
+ if (perUser.isPresent()) {
+ return perUser;
+ }
+
+ var systemWide = WindowsRegistry.readString(
+ WindowsRegistry.HKEY_LOCAL_MACHINE,
+ "SOFTWARE\\71445fac-d6ef-5436-9da7-5a323762d7f5",
+ "InstallLocation")
+ .map(p -> p + "\\Tabby.exe").map(Path::of);
+ return systemWide;
}
};
diff --git a/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java b/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java
index 28b78ea01..b127ded61 100644
--- a/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java
+++ b/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java
@@ -18,6 +18,8 @@ import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import lombok.SneakyThrows;
+import java.util.List;
+
import static io.xpipe.app.prefs.AppPrefs.group;
public class VaultCategory extends AppPrefsCategory {
@@ -69,6 +71,9 @@ public class VaultCategory extends AppPrefsCategory {
c.setPrefWidth(1000);
return c;
});
+ if (!pro) {
+ prefs.getProRequiredSettings().addAll(List.of(enable, remote));
+ }
return Category.of(
"vault",
group(
diff --git a/app/src/main/java/io/xpipe/app/storage/DataStoreColor.java b/app/src/main/java/io/xpipe/app/storage/DataStoreColor.java
index 7005bb248..117a44258 100644
--- a/app/src/main/java/io/xpipe/app/storage/DataStoreColor.java
+++ b/app/src/main/java/io/xpipe/app/storage/DataStoreColor.java
@@ -7,13 +7,13 @@ import lombok.Getter;
@Getter
public enum DataStoreColor {
@JsonProperty("red")
- RED("red", "\uD83D\uDFE5", Color.BLUE),
+ RED("red", "\uD83D\uDD34", Color.RED),
@JsonProperty("green")
- GREEN("green", "\uD83D\uDFE9", Color.BLUE),
+ GREEN("green", "\uD83D\uDFE2", Color.GREEN),
@JsonProperty("yellow")
- YELLOW("yellow", "\uD83D\uDFE8", Color.BLUE),
+ YELLOW("yellow", "\uD83D\uDFE1", Color.YELLOW),
@JsonProperty("blue")
BLUE("blue", "\uD83D\uDD35", Color.BLUE);
diff --git a/app/src/main/java/io/xpipe/app/util/LicenseProvider.java b/app/src/main/java/io/xpipe/app/util/LicenseProvider.java
index 212266a9f..407380558 100644
--- a/app/src/main/java/io/xpipe/app/util/LicenseProvider.java
+++ b/app/src/main/java/io/xpipe/app/util/LicenseProvider.java
@@ -1,5 +1,6 @@
package io.xpipe.app.util;
+import io.xpipe.app.ext.ExtensionException;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.storage.GitStorageHandler;
import io.xpipe.core.process.ShellControl;
@@ -21,7 +22,7 @@ public abstract class LicenseProvider {
public void init(ModuleLayer layer) {
INSTANCE = ServiceLoader.load(layer, LicenseProvider.class).stream()
.map(ServiceLoader.Provider::get)
- .findFirst().orElseThrow();
+ .findFirst().orElseThrow(() -> ExtensionException.corrupt("Missing license provider."));
}
@Override
@@ -35,6 +36,8 @@ public abstract class LicenseProvider {
}
}
+ public abstract boolean hasLicense();
+
public abstract LicensedFeature getFeature(String id);
public abstract void handleShellControl(ShellControl sc);
diff --git a/app/src/main/java/io/xpipe/app/util/ScanAlert.java b/app/src/main/java/io/xpipe/app/util/ScanAlert.java
index 6c4c414ee..2b8a37336 100644
--- a/app/src/main/java/io/xpipe/app/util/ScanAlert.java
+++ b/app/src/main/java/io/xpipe/app/util/ScanAlert.java
@@ -41,6 +41,10 @@ public class ScanAlert {
private static void showForShellStore(DataStoreEntry initial) {
show(initial, (DataStoreEntry entry) -> {
try (var sc = ((ShellStore) entry.getStore()).control().start()) {
+ if (!sc.getShellDialect().isSupportedShell()) {
+ return null;
+ }
+
var providers = ScanProvider.getAll();
var applicable = new ArrayList();
for (ScanProvider scanProvider : providers) {
diff --git a/app/src/main/resources/io/xpipe/app/resources/img/logo/logo_macos_128x128.png b/app/src/main/resources/io/xpipe/app/resources/img/logo/logo_macos_128x128.png
new file mode 100644
index 000000000..6cbdf894f
Binary files /dev/null and b/app/src/main/resources/io/xpipe/app/resources/img/logo/logo_macos_128x128.png differ
diff --git a/app/src/main/resources/io/xpipe/app/resources/img/logo/logo_macos_tray_24x24.png b/app/src/main/resources/io/xpipe/app/resources/img/logo/logo_macos_tray_24x24.png
new file mode 100644
index 000000000..130262ef6
Binary files /dev/null and b/app/src/main/resources/io/xpipe/app/resources/img/logo/logo_macos_tray_24x24.png differ
diff --git a/app/src/main/resources/io/xpipe/app/resources/style/bookmark.css b/app/src/main/resources/io/xpipe/app/resources/style/bookmark.css
index ff400b8ca..9603bbd9a 100644
--- a/app/src/main/resources/io/xpipe/app/resources/style/bookmark.css
+++ b/app/src/main/resources/io/xpipe/app/resources/style/bookmark.css
@@ -15,10 +15,8 @@
.bookmark-list .store-section-mini-comp .item:selected {
-fx-border-width: 1px;
-fx-border-radius: 4px;
--fx-border-insets: 0px 10px 0 0;
--fx-border-color: -color-accent-muted;
--fx-background-color: -color-bg-default;
+-fx-background-color: transparent, -color-accent-emphasis, -color-bg-overlay;
-fx-background-radius: 4px;
--fx-background-insets: 0px 10px 0 0;
+-fx-background-insets: 1 11 1 1, 2 12 2 2, 3 13 3 3;
-fx-padding: 0.25em 0.4em 0.25em 0.4em;
}
diff --git a/app/src/main/resources/io/xpipe/app/resources/style/browser.css b/app/src/main/resources/io/xpipe/app/resources/style/browser.css
index 05ba6750c..fabbbd361 100644
--- a/app/src/main/resources/io/xpipe/app/resources/style/browser.css
+++ b/app/src/main/resources/io/xpipe/app/resources/style/browser.css
@@ -4,6 +4,14 @@
-fx-padding: 1em;
}
+.browser .welcome .button {
+ -fx-border-radius: 4px;
+}
+
+.browser .welcome .button:hover {
+ -fx-background-color: -color-neutral-muted;
+}
+
.browser .tile > * {
-fx-padding: 0.6em 0 0.6em 0;
}
@@ -163,9 +171,10 @@
}
.browser .context-menu {
--fx-padding: 0;
--fx-background-radius: 1px;
--fx-border-color: -color-neutral-muted;
+ -fx-padding: 12 0 12 0;
+ -fx-background-radius: 8px;
+ -fx-border-radius: 8px;
+ -fx-border-color: -color-border-default;
}
.browser .tab-pane {
diff --git a/app/src/main/resources/io/xpipe/app/resources/style/popup-menu.css b/app/src/main/resources/io/xpipe/app/resources/style/popup-menu.css
index 061551827..57d9b4504 100644
--- a/app/src/main/resources/io/xpipe/app/resources/style/popup-menu.css
+++ b/app/src/main/resources/io/xpipe/app/resources/style/popup-menu.css
@@ -1,6 +1,3 @@
-.context-menu {
--fx-background-radius: 5px;
-}
.header-menu-item {
-fx-background-color: white;
@@ -45,4 +42,6 @@
.context-menu * .context-menu {
-fx-padding: 0;
+ -fx-background-radius: 0;
+ -fx-border-radius: 0;
}
diff --git a/app/src/main/resources/io/xpipe/app/resources/style/store-entry-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/store-entry-comp.css
index 364006cb4..334620049 100644
--- a/app/src/main/resources/io/xpipe/app/resources/style/store-entry-comp.css
+++ b/app/src/main/resources/io/xpipe/app/resources/style/store-entry-comp.css
@@ -66,8 +66,12 @@
-fx-background-radius: 0;
}
+.store-entry-comp:hover:armed {
+-fx-background-color: derive(-color-neutral-muted, 25%);
+}
+
.store-entry-comp:hover {
--fx-background-color: -color-neutral-muted;
+ -fx-background-color: -color-neutral-muted;
}
.store-entry-comp .button-bar, .store-entry-comp .dropdown-comp {
@@ -116,11 +120,17 @@
-fx-effect: dropshadow(three-pass-box, -color-shadow-default, 2px, 0.5, 0, 1);
}
-.store-entry-section-comp {
+.store-entry-section-comp:root {
-fx-border-radius: 4px;
-fx-background-radius: 4px;
}
+.store-entry-section-comp:sub:expanded {
+ -fx-border-radius: 4 0 0 4;
+ -fx-border-width: 1 0 1 1;
+ -fx-background-radius: 4 0 0 4;
+}
+
.root.nord .store-entry-section-comp {
-fx-border-radius: 0;
-fx-background-radius: 0;
diff --git a/build.gradle b/build.gradle
index 2d8813bd6..d2880a8be 100644
--- a/build.gradle
+++ b/build.gradle
@@ -45,7 +45,7 @@ project.ext {
kebapProductName = isStage ? 'xpipe-ptb' : 'xpipe'
publisher = 'XPipe UG (haftungsbeschränkt)'
shortDescription = 'Your entire server infrastructure at your fingertips'
- longDescription = 'XPipe is a new type of shell connection hub and remote file manager that allows you to access your entire sever infrastructure from your local machine. It works on top of your installed command-line programs that you normally use to connect and does not require any setup on your remote systems.'
+ longDescription = 'XPipe is a new type of shell connection hub and remote file manager that allows you to access your entire server infrastructure from your local machine. It works on top of your installed command-line programs that you normally use to connect and does not require any setup on your remote systems.'
website = 'https://xpipe.io'
sourceWebsite = 'https://github.com/xpipe-io/xpipe'
authors = 'Christopher Schnick'
diff --git a/core/src/main/java/io/xpipe/core/process/ScriptSnippet.java b/core/src/main/java/io/xpipe/core/process/ScriptSnippet.java
new file mode 100644
index 000000000..5d99a0e46
--- /dev/null
+++ b/core/src/main/java/io/xpipe/core/process/ScriptSnippet.java
@@ -0,0 +1,35 @@
+package io.xpipe.core.process;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Getter;
+
+public interface ScriptSnippet {
+
+ @Getter
+ public static enum ExecutionType {
+ @JsonProperty("dumbOnly")
+ DUMB_ONLY("dumbOnly"),
+ @JsonProperty("terminalOnly")
+ TERMINAL_ONLY("terminalOnly"),
+ @JsonProperty("both")
+ BOTH("both");
+
+ private final String id;
+
+ ExecutionType(String id) {
+ this.id = id;
+ }
+
+ public boolean runInDumb() {
+ return this == DUMB_ONLY || this == BOTH;
+ }
+
+ public boolean runInTerminal() {
+ return this == TERMINAL_ONLY || this == BOTH;
+ }
+ }
+
+ String content(ShellControl shellControl);
+
+ ExecutionType executionType();
+}
diff --git a/core/src/main/java/io/xpipe/core/process/ShellControl.java b/core/src/main/java/io/xpipe/core/process/ShellControl.java
index 0da6ed231..df3e0e35d 100644
--- a/core/src/main/java/io/xpipe/core/process/ShellControl.java
+++ b/core/src/main/java/io/xpipe/core/process/ShellControl.java
@@ -15,6 +15,8 @@ import java.util.function.Predicate;
public interface ShellControl extends ProcessControl {
+ List getInitCommands();
+
ShellControl withTargetTerminalShellDialect(ShellDialect d);
ShellDialect getTargetTerminalShellDialect();
@@ -153,11 +155,7 @@ public interface ShellControl extends ProcessControl {
}
ShellControl elevationPassword(FailableSupplier value);
- ShellControl initWith(String cmds);
-
- ShellControl initWithDumb(String cmds);
-
- ShellControl initWithTerminal(String cmds);
+ ShellControl initWith(ScriptSnippet snippet);
ShellControl additionalTimeout(int ms);
diff --git a/core/src/main/java/io/xpipe/core/process/ShellDialect.java b/core/src/main/java/io/xpipe/core/process/ShellDialect.java
index 8819305dd..e88ae99c7 100644
--- a/core/src/main/java/io/xpipe/core/process/ShellDialect.java
+++ b/core/src/main/java/io/xpipe/core/process/ShellDialect.java
@@ -26,6 +26,10 @@ public interface ShellDialect {
.collect(Collectors.joining(" "));
}
+ default boolean isSupportedShell() {
+ return true;
+ }
+
default boolean isSelectable() {
return true;
}
@@ -40,7 +44,7 @@ public interface ShellDialect {
String getCatchAllVariable();
- CommandControl queryVersion(ShellControl shellControl);
+ String queryVersion(ShellControl shellControl) throws Exception;
CommandControl prepareUserTempDirectory(ShellControl shellControl, String directory);
diff --git a/core/src/main/java/io/xpipe/core/process/ShellDialects.java b/core/src/main/java/io/xpipe/core/process/ShellDialects.java
index 499306534..f7f8c0dab 100644
--- a/core/src/main/java/io/xpipe/core/process/ShellDialects.java
+++ b/core/src/main/java/io/xpipe/core/process/ShellDialects.java
@@ -20,6 +20,8 @@ public class ShellDialects {
public static ShellDialect ZSH;
public static ShellDialect CSH;
public static ShellDialect FISH;
+ public static ShellDialect UNSUPPORTED;
+ public static ShellDialect CISCO;
public static class Loader implements ModuleLayerLoader {
@@ -40,6 +42,8 @@ public class ShellDialects {
CSH = byName("csh");
ASH = byName("ash");
SH = byName("sh");
+ UNSUPPORTED = byName("unsupported");
+ CISCO = byName("cisco");
}
@Override
diff --git a/core/src/main/java/io/xpipe/core/process/SimpleScriptSnippet.java b/core/src/main/java/io/xpipe/core/process/SimpleScriptSnippet.java
new file mode 100644
index 000000000..21472125c
--- /dev/null
+++ b/core/src/main/java/io/xpipe/core/process/SimpleScriptSnippet.java
@@ -0,0 +1,26 @@
+package io.xpipe.core.process;
+
+import lombok.NonNull;
+
+public class SimpleScriptSnippet implements ScriptSnippet {
+
+ @NonNull
+ private final String content;
+ @NonNull
+ private final ExecutionType executionType;
+
+ public SimpleScriptSnippet(@NonNull String content, @NonNull ExecutionType executionType) {
+ this.content = content;
+ this.executionType = executionType;
+ }
+
+ @Override
+ public String content(ShellControl shellControl) {
+ return content;
+ }
+
+ @Override
+ public ExecutionType executionType() {
+ return executionType;
+ }
+}
diff --git a/dist/changelogs/1.7.3.md b/dist/changelogs/1.7.3.md
new file mode 100644
index 000000000..c96a7fefa
--- /dev/null
+++ b/dist/changelogs/1.7.3.md
@@ -0,0 +1,21 @@
+## Changes in 1.7.3
+
+- Use newly created macOS app icons that better fit in with the general macOS aesthetic
+- Fix connection freezing when sudo askpass dialog was cancelled by now just executing commands as normal user
+- Fix Tabby installation not being detected on Windows if it was system-wide instead of per-user
+- Fix some settings values being incorrectly labelled as professional-only
+- Fix application restart for license activation not working
+- Fix open browser tab list being broken when reordering tabs by dragging
+- Fix browser welcome screen connection list jumping around
+- Fix connection sorting sometimes not working
+- Fix connections being duplicated in all connections overview
+- Fix move to category functionality being broken
+- Fix bring scripts functionality throwing errors on script setup
+- Fix scripts possibly being called multiple times when set as default and as dependencies
+- Improve scripts help documentation
+- Improve styling in some areas
+
+## Previous changes in 1.7
+
+- https://github.com/xpipe-io/xpipe/releases/tag/1.7.2
+- https://github.com/xpipe-io/xpipe/releases/tag/1.7.1
\ No newline at end of file
diff --git a/dist/logo/logo.icns b/dist/logo/logo.icns
index 407b867ea..7d9ea367c 100644
Binary files a/dist/logo/logo.icns and b/dist/logo/logo.icns differ
diff --git a/dist/logo/logo.iconset/icon_128x128.png b/dist/logo/logo.iconset/icon_128x128.png
index 0fbcf086b..6cbdf894f 100644
Binary files a/dist/logo/logo.iconset/icon_128x128.png and b/dist/logo/logo.iconset/icon_128x128.png differ
diff --git a/dist/logo/logo.iconset/icon_128x128@2.png b/dist/logo/logo.iconset/icon_128x128@2.png
index 67ff78ad5..0f36d2877 100644
Binary files a/dist/logo/logo.iconset/icon_128x128@2.png and b/dist/logo/logo.iconset/icon_128x128@2.png differ
diff --git a/dist/logo/logo.iconset/icon_16x16.png b/dist/logo/logo.iconset/icon_16x16.png
index d05eb5223..a2608ae1a 100644
Binary files a/dist/logo/logo.iconset/icon_16x16.png and b/dist/logo/logo.iconset/icon_16x16.png differ
diff --git a/dist/logo/logo.iconset/icon_16x16@2.png b/dist/logo/logo.iconset/icon_16x16@2.png
index 832763c47..c982016ce 100644
Binary files a/dist/logo/logo.iconset/icon_16x16@2.png and b/dist/logo/logo.iconset/icon_16x16@2.png differ
diff --git a/dist/logo/logo.iconset/icon_256x256.png b/dist/logo/logo.iconset/icon_256x256.png
index 67ff78ad5..0f36d2877 100644
Binary files a/dist/logo/logo.iconset/icon_256x256.png and b/dist/logo/logo.iconset/icon_256x256.png differ
diff --git a/dist/logo/logo.iconset/icon_256x256@2.png b/dist/logo/logo.iconset/icon_256x256@2.png
index 350f87fff..7b38dee12 100644
Binary files a/dist/logo/logo.iconset/icon_256x256@2.png and b/dist/logo/logo.iconset/icon_256x256@2.png differ
diff --git a/dist/logo/logo.iconset/icon_32x32.png b/dist/logo/logo.iconset/icon_32x32.png
index 832763c47..c982016ce 100644
Binary files a/dist/logo/logo.iconset/icon_32x32.png and b/dist/logo/logo.iconset/icon_32x32.png differ
diff --git a/dist/logo/logo.iconset/icon_32x32@2.png b/dist/logo/logo.iconset/icon_32x32@2.png
index c11dbe7b0..48c5e3b07 100644
Binary files a/dist/logo/logo.iconset/icon_32x32@2.png and b/dist/logo/logo.iconset/icon_32x32@2.png differ
diff --git a/dist/logo/logo.iconset/icon_512x512.png b/dist/logo/logo.iconset/icon_512x512.png
index 350f87fff..7b38dee12 100644
Binary files a/dist/logo/logo.iconset/icon_512x512.png and b/dist/logo/logo.iconset/icon_512x512.png differ
diff --git a/dist/logo/logo.iconset/icon_512x512@2.png b/dist/logo/logo.iconset/icon_512x512@2.png
index 4095e850b..45af483fd 100644
Binary files a/dist/logo/logo.iconset/icon_512x512@2.png and b/dist/logo/logo.iconset/icon_512x512@2.png differ
diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStore.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStore.java
index f50a70f37..5891cad06 100644
--- a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStore.java
+++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStore.java
@@ -1,9 +1,13 @@
package io.xpipe.ext.base.script;
+import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage;
+import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.Validators;
+import io.xpipe.core.process.ScriptSnippet;
import io.xpipe.core.process.ShellControl;
+import io.xpipe.core.process.SimpleScriptSnippet;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.DataStoreState;
import io.xpipe.core.store.FileNames;
@@ -18,8 +22,6 @@ import lombok.extern.jackson.Jacksonized;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
-import java.util.Objects;
-import java.util.stream.Collectors;
@SuperBuilder
@Getter
@@ -31,56 +33,67 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore,
}
public static ShellControl controlWithScripts(ShellControl pc, List> initScripts, List> bringScripts) {
- pc.onInit(shellControl -> {
- var initFlattened = flatten(initScripts);
- var scripts = initFlattened.stream()
- .map(simpleScriptStore -> simpleScriptStore.prepareDumbScript(shellControl))
- .filter(Objects::nonNull)
- .collect(Collectors.joining("\n"));
- if (!scripts.isBlank()) {
- shellControl.executeSimpleBooleanCommand(scripts);
- }
+ var initFlattened = flatten(initScripts);
+ var bringFlattened = flatten(bringScripts);
- var terminalCommands = initFlattened.stream()
- .map(simpleScriptStore -> simpleScriptStore.prepareTerminalScript(shellControl))
- .filter(Objects::nonNull)
- .collect(Collectors.joining("\n"));
- if (!terminalCommands.isBlank()) {
- shellControl.initWithTerminal(terminalCommands);
- }
- });
pc.onInit(shellControl -> {
- var bringFlattened = flatten(bringScripts);
+ passInitScripts(pc, initFlattened);
+
var dir = initScriptsDirectory(shellControl, bringFlattened);
if (dir != null) {
- shellControl.initWithTerminal(shellControl.getShellDialect().appendToPathVariableCommand(dir));
+ shellControl.initWith(new SimpleScriptSnippet(shellControl.getShellDialect().appendToPathVariableCommand(dir), ScriptSnippet.ExecutionType.TERMINAL_ONLY));
}
});
return pc;
}
+ private static void passInitScripts(ShellControl pc, List scriptStores) throws Exception {
+ scriptStores.forEach(simpleScriptStore -> {
+ if (pc.getInitCommands().contains(simpleScriptStore)) {
+ return;
+ }
+
+ if (!simpleScriptStore.getMinimumDialect().isCompatibleTo(pc.getShellDialect())) {
+ return;
+ }
+
+ pc.initWith(simpleScriptStore);
+ });
+ }
+
private static String initScriptsDirectory(ShellControl proc, List scriptStores) throws Exception {
- if (scriptStores.size() == 0) {
+ if (scriptStores.isEmpty()) {
return null;
}
- var refs = scriptStores.stream().map(scriptStore -> {
+ var applicable = scriptStores.stream().filter(simpleScriptStore -> simpleScriptStore.getMinimumDialect().isCompatibleTo(proc.getShellDialect())).toList();
+ if (applicable.isEmpty()) {
+ return null;
+ }
+
+ var refs = applicable.stream().map(scriptStore -> {
return DataStorage.get().getStoreEntries().stream().filter(dataStoreEntry -> dataStoreEntry.getStore() == scriptStore).findFirst().orElseThrow().ref();
}).toList();
var hash = refs.stream().mapToInt(value -> value.get().getName().hashCode() + value.getStore().hashCode()).sum();
var xpipeHome = XPipeInstallation.getDataDir(proc);
- var targetDir = FileNames.join(xpipeHome, "scripts");
+ var targetDir = FileNames.join(xpipeHome, "scripts", proc.getShellDialect().getId());
var hashFile = FileNames.join(targetDir, "hash");
var d = proc.getShellDialect();
if (d.createFileExistsCommand(proc, hashFile).executeAndCheck()) {
var read = d.getFileReadCommand(proc, hashFile).readStdoutOrThrow();
- var readHash = Integer.parseInt(read);
- if (hash == readHash) {
- return targetDir;
+ try {
+ var readHash = Integer.parseInt(read);
+ if (hash == readHash) {
+ return targetDir;
+ }
+ } catch (NumberFormatException e) {
+ ErrorEvent.fromThrowable(e).omit().handle();
}
}
- d.deleteFileOrDirectory(proc, targetDir).execute();
+ if (d.directoryExists(proc, targetDir).executeAndCheck()) {
+ d.deleteFileOrDirectory(proc, targetDir).execute();
+ }
proc.executeSimpleCommand(d.getMkdirsCommand(targetDir));
for (DataStoreEntryRef scriptStore : refs) {
@@ -90,7 +103,9 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore,
d.createScriptTextFileWriteCommand(proc, content, scriptFile).execute();
var chmod = d.getScriptPermissionsCommand(scriptFile);
- proc.executeSimpleBooleanCommand(chmod);
+ if (chmod != null) {
+ proc.executeSimpleBooleanCommand(chmod);
+ }
}
d.createTextFileWriteCommand(proc, String.valueOf(hash), hashFile).execute();
@@ -101,7 +116,7 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore,
return DataStorage.get().getStoreEntries().stream()
.filter(dataStoreEntry -> dataStoreEntry.getStore() instanceof ScriptStore scriptStore
&& scriptStore.getState().isDefault())
- .map(e -> e.ref())
+ .map(DataStoreEntry::ref)
.toList();
}
@@ -109,7 +124,7 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore,
return DataStorage.get().getStoreEntries().stream()
.filter(dataStoreEntry -> dataStoreEntry.getStore() instanceof ScriptStore scriptStore
&& scriptStore.getState().isBringToShell())
- .map(e -> e.ref())
+ .map(DataStoreEntry::ref)
.toList();
}
@@ -155,12 +170,6 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore,
// }
}
- public LinkedHashSet getFlattenedScripts() {
- var set = new LinkedHashSet();
- queryFlattenedScripts(set);
- return set;
- }
-
protected abstract void queryFlattenedScripts(LinkedHashSet all);
public abstract List> getEffectiveScripts();
diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStore.java b/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStore.java
index 9860188b7..677312a76 100644
--- a/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStore.java
+++ b/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStore.java
@@ -1,10 +1,10 @@
package io.xpipe.ext.base.script;
-import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.ScriptHelper;
import io.xpipe.app.util.Validators;
+import io.xpipe.core.process.ScriptSnippet;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialect;
import lombok.Getter;
@@ -13,21 +13,14 @@ import lombok.extern.jackson.Jacksonized;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Objects;
import java.util.stream.Collectors;
@SuperBuilder
@Getter
@Jacksonized
@JsonTypeName("script")
-public class SimpleScriptStore extends ScriptStore {
-
- public String prepareDumbScript(ShellControl shellControl) {
- return assemble(shellControl, ExecutionType.DUMB_ONLY);
- }
-
- public String prepareTerminalScript(ShellControl shellControl) {
- return assemble(shellControl, ExecutionType.TERMINAL_ONLY);
- }
+public class SimpleScriptStore extends ScriptStore implements ScriptSnippet {
private String assemble(ShellControl shellControl, ExecutionType type) {
var targetType = type == ExecutionType.TERMINAL_ONLY
@@ -51,39 +44,35 @@ public class SimpleScriptStore extends ScriptStore {
@Override
public List> getEffectiveScripts() {
return scripts != null
- ? scripts.stream().filter(scriptStore -> scriptStore != null).toList()
+ ? scripts.stream().filter(Objects::nonNull).toList()
: List.of();
}
public void queryFlattenedScripts(LinkedHashSet all) {
+ // Prevent loop
all.add(this);
getEffectiveScripts().stream()
.filter(scriptStoreDataStoreEntryRef -> !all.contains(scriptStoreDataStoreEntryRef.getStore()))
.forEach(scriptStoreDataStoreEntryRef -> {
scriptStoreDataStoreEntryRef.getStore().queryFlattenedScripts(all);
});
+ all.remove(this);
+ all.add(this);
}
- @Getter
- public enum ExecutionType {
- @JsonProperty("dumbOnly")
- DUMB_ONLY("dumbOnly"),
- @JsonProperty("terminalOnly")
- TERMINAL_ONLY("terminalOnly"),
- @JsonProperty("both")
- BOTH("both");
+ @Override
+ public String content(ShellControl shellControl) {
+ return assemble(shellControl, executionType);
+ }
- private final String id;
-
- ExecutionType(String id) {
- this.id = id;
- }
+ @Override
+ public ScriptSnippet.ExecutionType executionType() {
+ return executionType;
}
private final ShellDialect minimumDialect;
private final String commands;
private final ExecutionType executionType;
- private final boolean requiresElevation;
@Override
public void checkComplete() throws Exception {
diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java
index 988c69987..e7e4ea120 100644
--- a/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java
+++ b/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java
@@ -137,7 +137,6 @@ public class SimpleScriptStoreProvider implements DataStoreProvider {
new SimpleListProperty<>(FXCollections.observableArrayList(new ArrayList<>(st.getEffectiveScripts())));
Property commandProp = new SimpleObjectProperty<>(st.getCommands());
var type = new SimpleObjectProperty<>(st.getExecutionType());
- var requiresElevationProperty = new SimpleBooleanProperty(st.isRequiresElevation());
Comp> choice = (Comp>) Class.forName(
AppExtensionManager.getInstance()
@@ -150,6 +149,7 @@ public class SimpleScriptStoreProvider implements DataStoreProvider {
return new OptionsBuilder()
.name("snippets")
.description("snippetsDescription")
+ .longDescription("base:scriptDependencies")
.addComp(
new DataStoreListChoiceComp<>(
others,
@@ -159,6 +159,7 @@ public class SimpleScriptStoreProvider implements DataStoreProvider {
others)
.name("minimumShellDialect")
.description("minimumShellDialectDescription")
+ .longDescription("base:scriptCompatibility")
.addComp(choice, dialect)
.nonNull()
.name("scriptContents")
@@ -175,10 +176,6 @@ public class SimpleScriptStoreProvider implements DataStoreProvider {
.description("executionTypeDescription")
.longDescription("base:executionType")
.addComp(new ScriptStoreTypeChoiceComp(type), type)
- .name("shouldElevate")
- .description("shouldElevateDescription")
- .longDescription("proc:elevation")
- .addToggle(requiresElevationProperty)
.name("scriptGroup")
.description("scriptGroupDescription")
.addComp(
@@ -195,7 +192,6 @@ public class SimpleScriptStoreProvider implements DataStoreProvider {
.description(st.getDescription())
.commands(commandProp.getValue())
.executionType(type.get())
- .requiresElevation(requiresElevationProperty.get())
.build();
},
store)
@@ -210,8 +206,7 @@ public class SimpleScriptStoreProvider implements DataStoreProvider {
@Override
public ObservableValue informationString(StoreEntryWrapper wrapper) {
SimpleScriptStore scriptStore = wrapper.getEntry().getStore().asNeeded();
- return new SimpleStringProperty((scriptStore.isRequiresElevation() ? "Elevated " : "")
- + (scriptStore.getMinimumDialect() != null
+ return new SimpleStringProperty((scriptStore.getMinimumDialect() != null
? scriptStore.getMinimumDialect().getDisplayName() + " "
: "")
+ (scriptStore.getExecutionType() == SimpleScriptStore.ExecutionType.TERMINAL_ONLY
@@ -273,7 +268,6 @@ public class SimpleScriptStoreProvider implements DataStoreProvider {
return SimpleScriptStore.builder()
.scripts(List.of())
.executionType(SimpleScriptStore.ExecutionType.TERMINAL_ONLY)
- .requiresElevation(false)
.build();
}
diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/executionType_en.md b/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/executionType_en.md
index c34e58bf5..185b08e88 100644
--- a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/executionType_en.md
+++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/executionType_en.md
@@ -1,14 +1,17 @@
## Execution types
-There are two distinct execution phases when XPipe connects to a system.
-The first connection is made in the background in a dumb terminal.
-Only afterward will a separate connection be made in the actual terminal.
+There are two distinct execution types when XPipe connects to a system.
-The file browser for example entirely uses the dumb background mode to handle its operations, so if you want your script environment to apply to the file browser session, it should run in the dumb mode.
+### Dumb terminals
-If you want the script to be run when you open the connection in a terminal, then choose the terminal mode.
-
-### Blocking commands
+The first connection to a system is made in the background in a dumb terminal.
Blocking commands that require user input can freeze the shell process when XPipe starts it up internally first in the background.
To avoid this, you should only call these blocking commands in the terminal mode.
+
+The file browser for example entirely uses the dumb background mode to handle its operations, so if you want your script environment to apply to the file browser session, it should run in the dumb mode.
+
+### Proper terminals
+
+After a dumb terminal connection has succeeded, XPipe will open a separate connection in the actual terminal.
+If you want the script to be run when you open the connection in a terminal, then choose the terminal mode.
diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/scriptCompatibility_en.md b/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/scriptCompatibility_en.md
new file mode 100644
index 000000000..80bcdc2d7
--- /dev/null
+++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/scriptCompatibility_en.md
@@ -0,0 +1,13 @@
+## Script compatibility
+
+The shell type controls where this script can be run.
+Aside from an exact match, i.e. running a `zsh` script in `zsh`, XPipe will also include wider compatibility checking.
+
+### Posix Shells
+
+Any script declared as a `sh` script is able to run in any posix-related shell environment such as `bash` or `zsh`.
+If you intend to run a basic script on many different systems, then using only `sh` syntax scripts is the best solution for that.
+
+### PowerShell
+
+Scripts declared as normal PowerShell scripts are also able to run in PowerShell Core environments.
diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/scriptDependencies_en.md b/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/scriptDependencies_en.md
new file mode 100644
index 000000000..6866d6739
--- /dev/null
+++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/scriptDependencies_en.md
@@ -0,0 +1,5 @@
+## Script dependencies
+
+The scripts and script groups to run first. If an entire group is made a dependency, all scripts in this group will be considered as dependencies.
+
+The resolved dependency graph of scripts is flattened, filtered, and made unique, i.e. only compatible scripts will be run and if script would be executed multiple times, it will only be run the first time.
diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/script_en.md b/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/script_en.md
index b24aea275..33db540a9 100644
--- a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/script_en.md
+++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/script_en.md
@@ -1,3 +1,5 @@
-The contents of the script to run. You can choose to either edit this in place or use the external edit button in the top right corner to launch an external text editor.
+## Script contents
+
+The contents of the script to run. You can choose to either edit this in-place or use the external edit button in the top right corner to launch an external text editor.
You don't have to specify a shebang line for shells that support it, one is added automatically with the appropriate shell type.
diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_en.properties b/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_en.properties
index 01bbddb9d..aa4c88c98 100644
--- a/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_en.properties
+++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/lang/translations_en.properties
@@ -72,9 +72,9 @@ scriptContentsDescription=The script commands to execute
snippets=Script dependencies
snippetsDescription=Other scripts to run first
snippetsDependenciesDescription=All possible scripts that should be run if applicable
-isDefault=Enable in all compatible shells
+isDefault=Run on init in all compatible shells
bringToShells=Bring to all compatible shells
-isDefaultGroup=Enable all group scripts
+isDefaultGroup=Run all group scripts on shell init
executionType=Execution type
executionTypeDescription=When to run this snippet
minimumShellDialect=Shell type
diff --git a/version b/version
index 0a182f2e3..243008ecf 100644
--- a/version
+++ b/version
@@ -1 +1 @@
-1.7.2
\ No newline at end of file
+1.7.3-3
\ No newline at end of file