Merge branch 1.7.3 into master

This commit is contained in:
crschnick 2023-11-04 05:36:47 +00:00
parent 8ef97ccc9f
commit bc7bde024a
59 changed files with 449 additions and 276 deletions

4
.gitignore vendored
View file

@ -1,6 +1,8 @@
.gradle/ .gradle/
build/ build/
.idea .idea/*
!.idea/codeStyles
!.idea/inspectionProfiles
lib/ lib/
dev.properties dev.properties
extensions.txt extensions.txt

View file

@ -1,8 +1,6 @@
<img src="https://github.com/xpipe-io/xpipe/assets/72509152/88d750f3-8469-4c51-bb64-5b264b0e9d47" alt="drawing" width="250"/> <img src="https://github.com/xpipe-io/xpipe/assets/72509152/88d750f3-8469-4c51-bb64-5b264b0e9d47" alt="drawing" width="250"/>
### 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 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 that you normally use to connect 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. 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 ## Connection Hub
- Easily connect to and access all kinds of remote connections in one place - 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 - 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 - Can create desktop shortcuts that automatically open remote connections in your terminal
- Group all your connections into hierarchical categories - 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 ## Remote File Manager
- Interact with the file system of any remote system using a workflow optimized for professionals - 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 - 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 ## Terminal Launcher
@ -44,13 +43,22 @@ It currently supports:
<br> <br>
<p align="center"> <p align="center">
<img src="https://github.com/xpipe-io/xpipe/assets/72509152/f3d29909-acd7-4568-a625-0667d936ef2b" alt="Terminal launcher"/> <img src="https://github.com/xpipe-io/xpipe/assets/72509152/02351317-f25d-4af3-8116-bc3b4fb92312" alt="Terminal launcher"/>
</p> </p>
<br> <br>
## 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 ### 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) bash <(curl -sL https://raw.githubusercontent.com/xpipe-io/xpipe/master/get-xpipe.sh)
``` ```
### Package managers ### Package managers
Alternatively, you can also use your favorite package manager (if supported): 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. 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 ## Further information

View file

@ -1,7 +1,10 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import atlantafx.base.theme.Styles; 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.core.AppFont;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FilterComp; import io.xpipe.app.fxcomps.impl.FilterComp;
@ -31,14 +34,12 @@ import java.util.function.Predicate;
final class BrowserBookmarkList extends SimpleComp { final class BrowserBookmarkList extends SimpleComp {
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
public static final Timer DROP_TIMER = new Timer("dnd", true); 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 Point2D lastOver = new Point2D(-1, -1);
private TimerTask activeTask; private TimerTask activeTask;
private final BrowserModel model;
BrowserBookmarkList(BrowserModel model) { BrowserBookmarkList(BrowserModel model) {
this.model = model; this.model = model;
} }
@ -48,28 +49,20 @@ final class BrowserBookmarkList extends SimpleComp {
var filterText = new SimpleStringProperty(); var filterText = new SimpleStringProperty();
var open = PlatformThread.sync(model.getSelected()); var open = PlatformThread.sync(model.getSelected());
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> { Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
return (storeEntryWrapper.getEntry().getStore() instanceof ShellStore return (storeEntryWrapper.getEntry().getStore() instanceof ShellStore ||
|| storeEntryWrapper.getEntry().getStore() instanceof FixedHierarchyStore) storeEntryWrapper.getEntry().getStore() instanceof FixedHierarchyStore) && storeEntryWrapper.getEntry().getValidity().isUsable();
&& storeEntryWrapper.getEntry().getValidity().isUsable();
}; };
var selectedCategory = new SimpleObjectProperty<>(StoreViewState.get().getActiveCategory().getValue()); var selectedCategory = new SimpleObjectProperty<>(StoreViewState.get().getActiveCategory().getValue());
var section = StoreSectionMiniComp.createList( var section = StoreSectionMiniComp.createList(
StoreSection.createTopLevel( StoreSection.createTopLevel(StoreViewState.get().getAllEntries(), applicable, filterText, selectedCategory), (s, comp) -> {
StoreViewState.get().getAllEntries(), applicable, filterText, selectedCategory),
(s, comp) -> {
BooleanProperty busy = new SimpleBooleanProperty(false); BooleanProperty busy = new SimpleBooleanProperty(false);
comp.disable(Bindings.createBooleanBinding(() -> { comp.disable(Bindings.createBooleanBinding(() -> {
return busy.get() || !applicable.test(s.getWrapper()); return busy.get() || !applicable.test(s.getWrapper());
}, busy)); }, busy));
comp.apply(struc -> { comp.apply(struc -> {
open.addListener((observable, oldValue, newValue) -> { open.addListener((observable, oldValue, newValue) -> {
struc.get() struc.get().pseudoClassStateChanged(SELECTED,
.pseudoClassStateChanged( newValue != null && newValue.getEntry().get().equals(s.getWrapper().getEntry()));
SELECTED,
newValue != null
&& newValue.getEntry().get()
.equals(s.getWrapper()
.getEntry()));
}); });
struc.get().setOnAction(event -> { struc.get().setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
@ -90,19 +83,14 @@ final class BrowserBookmarkList extends SimpleComp {
}); });
}); });
}); });
var category = new DataStoreCategoryChoiceComp(StoreViewState.get().getAllConnectionsCategory(), StoreViewState.get().getActiveCategory(), selectedCategory) var category = new DataStoreCategoryChoiceComp(StoreViewState.get().getAllConnectionsCategory(), StoreViewState.get().getActiveCategory(),
.styleClass(Styles.LEFT_PILL) selectedCategory).styleClass(Styles.LEFT_PILL).grow(false, true);
.grow(false, true); var filter = new FilterComp(filterText).styleClass(Styles.RIGHT_PILL).hgrow().apply(struc -> {});
var filter =
new FilterComp(filterText).styleClass(Styles.RIGHT_PILL).hgrow().apply(struc -> {});
var top = new HorizontalComp(List.of(category, filter.hgrow())) var top = new HorizontalComp(List.of(category, filter.hgrow())).styleClass("categories").apply(struc -> {
.styleClass("categories")
.apply(struc -> {
AppFont.medium(struc.get()); AppFont.medium(struc.get());
struc.get().setFillHeight(true); struc.get().setFillHeight(true);
}) }).createRegion();
.createRegion();
var r = section.vgrow().createRegion(); var r = section.vgrow().createRegion();
var content = new VBox(top, r); var content = new VBox(top, r);
content.setFillWidth(true); content.setFillWidth(true);

View file

@ -170,30 +170,45 @@ public class BrowserComp extends SimpleComp {
var modifying = new SimpleBooleanProperty(); var modifying = new SimpleBooleanProperty();
// Handle selection from platform // Handle selection from platform
tabs.getSelectionModel().selectedIndexProperty().addListener((observable, oldValue, newValue) -> { tabs.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
if (modifying.get()) { if (modifying.get()) {
return; return;
} }
if (newValue.intValue() == -1) { if (newValue == null) {
model.getSelected().setValue(null); model.getSelected().setValue(null);
return; 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 // Handle selection from model
model.getSelected().addListener((observable, oldValue, newValue) -> { model.getSelected().addListener((observable, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> { PlatformThread.runLaterIfNeeded(() -> {
var index = model.getOpenFileSystems().indexOf(newValue); if (newValue == null) {
if (index == -1 || index >= tabs.getTabs().size()) {
tabs.getSelectionModel().select(null); tabs.getSelectionModel().select(null);
return; return;
} }
var tab = tabs.getTabs().get(index); var toSelect = map.entrySet().stream()
tabs.getSelectionModel().select(tab); .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);
}); });
}); });

View file

@ -24,6 +24,15 @@ import java.util.function.Consumer;
@Getter @Getter
public class BrowserModel { public class BrowserModel {
public static final BrowserModel DEFAULT = new BrowserModel(Mode.BROWSER);
private final Mode mode;
private final ObservableList<OpenFileSystemModel> openFileSystems = FXCollections.observableArrayList();
private final Property<OpenFileSystemModel> selected = new SimpleObjectProperty<>();
private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this);
private final ObservableList<BrowserEntry> selection = FXCollections.observableArrayList();
@Setter
private Consumer<List<FileStore>> onFinish;
public BrowserModel(Mode mode) { public BrowserModel(Mode mode) {
this.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<List<FileStore>> onFinish;
private final ObservableList<OpenFileSystemModel> openFileSystems = FXCollections.observableArrayList();
private final Property<OpenFileSystemModel> selected = new SimpleObjectProperty<>();
private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this);
private final ObservableList<BrowserEntry> selection = FXCollections.observableArrayList();
public void restoreState(BrowserSavedState state) { public void restoreState(BrowserSavedState state) {
state.getLastSystems().forEach(e -> { state.getLastSystems().forEach(e -> {
restoreState(e, null); restoreState(e, null);
@ -87,8 +62,7 @@ public class BrowserModel {
var list = new ArrayList<BrowserSavedState.Entry>(); var list = new ArrayList<BrowserSavedState.Entry>();
openFileSystems.forEach(model -> { openFileSystems.forEach(model -> {
if (DataStorage.get().getStoreEntries().contains(model.getEntry().get())) { if (DataStorage.get().getStoreEntries().contains(model.getEntry().get())) {
list.add(new BrowserSavedState.Entry( list.add(new BrowserSavedState.Entry(model.getEntry().get().getUuid(), model.getCurrentPath().get()));
model.getEntry().get().getUuid(), model.getCurrentPath().get()));
} }
}); });
@ -120,11 +94,8 @@ public class BrowserModel {
return; return;
} }
var stores = chosen.stream() var stores = chosen.stream().map(
.map(entry -> new FileStore( entry -> new FileStore(entry.getRawFileEntry().getFileSystem().getStore(), entry.getRawFileEntry().getPath())).toList();
entry.getRawFileEntry().getFileSystem().getStore(),
entry.getRawFileEntry().getPath()))
.toList();
onFinish.accept(stores); onFinish.accept(stores);
} }
@ -141,9 +112,7 @@ public class BrowserModel {
} }
public void openExistingFileSystemIfPresent(DataStoreEntryRef<? extends FileSystemStore> store) { public void openExistingFileSystemIfPresent(DataStoreEntryRef<? extends FileSystemStore> store) {
var found = openFileSystems.stream() var found = openFileSystems.stream().filter(model -> Objects.equals(model.getEntry(), store)).findFirst();
.filter(model -> Objects.equals(model.getEntry(), store))
.findFirst();
if (found.isPresent()) { if (found.isPresent()) {
selected.setValue(found.get()); selected.setValue(found.get());
} else { } else {
@ -176,9 +145,9 @@ public class BrowserModel {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
OpenFileSystemModel model; OpenFileSystemModel model;
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
// Prevent multiple calls from interfering with each other // Prevent multiple calls from interfering with each other
synchronized (BrowserModel.this) { synchronized (BrowserModel.this) {
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
model = new OpenFileSystemModel(this, store); model = new OpenFileSystemModel(this, store);
model.initFileSystem(); model.initFileSystem();
model.initSavedState(); 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;
}
}
} }

View file

@ -1,20 +1,25 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import atlantafx.base.controls.Spacer; import atlantafx.base.controls.Spacer;
import atlantafx.base.controls.Tile;
import atlantafx.base.theme.Styles; 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.comp.base.TileButtonComp;
import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.storage.DataStorage; 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.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Orientation; import javafx.geometry.Orientation;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Separator; import javafx.scene.control.Separator;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
@ -53,40 +58,45 @@ public class BrowserWelcomeComp extends SimpleComp {
var storeList = new VBox(); var storeList = new VBox();
storeList.setSpacing(8); storeList.setSpacing(8);
state.getLastSystems().forEach(e -> {
var list = FXCollections.observableList(state.getLastSystems().stream().filter(e -> {
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid()); var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
if (entry.isEmpty()) { if (entry.isEmpty()) {
return; return false;
} }
if (!entry.get().getValidity().isUsable()) { if (!entry.get().getValidity().isUsable()) {
return; return false;
} }
var graphic = return true;
entry.get().getProvider().getDisplayIconFileName(entry.get().getStore()); }).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); var view = PrettyImageHelper.ofFixedSquare(graphic, 45);
view.padding(new Insets(2, 8, 2, 8)); view.padding(new Insets(2, 8, 2, 8));
var tile = new Tile( var content =
DataStorage.get().getStoreDisplayName(entry.get()), JfxHelper.createNamedEntry(DataStorage.get().getStoreDisplayName(entry.get()), e.getPath(), graphic);
e.getPath(), var disable = new SimpleBooleanProperty();
view.createRegion()); return new ButtonComp(null, content, () -> {
tile.setActionHandler(() -> { ThreadHelper.runAsync(() -> {
model.restoreState(e, tile.disableProperty()); model.restoreState(e, disable);
}); });
storeList.getChildren().add(tile); }).disable(disable).styleClass("color-box").apply(struc -> struc.get().setMaxWidth(2000)).grow(true, false);
}); }).apply(struc -> {
VBox vBox = (VBox) struc.get().getContent();
var sp = new ScrollPane(storeList); vBox.setSpacing(10);
sp.setFitToWidth(true); }).createRegion();
var layout = new VBox(); var layout = new VBox();
layout.setMaxWidth(700); layout.getStyleClass().add("welcome");
layout.setPadding(new Insets(40, 40, 40, 50)); layout.setPadding(new Insets(40, 40, 40, 50));
layout.setSpacing(18); layout.setSpacing(18);
layout.getChildren().add(hbox); layout.getChildren().add(hbox);
layout.getChildren().add(new Separator(Orientation.HORIZONTAL)); 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)); layout.getChildren().add(new Separator(Orientation.HORIZONTAL));
var tile = new TileButtonComp("restore", "restoreAllSessions", "mdmz-restore", actionEvent -> { var tile = new TileButtonComp("restore", "restoreAllSessions", "mdmz-restore", actionEvent -> {

View file

@ -124,7 +124,7 @@ public class FileSystemHelper {
} }
if (!model.getFileSystem().directoryExists(path)) { 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); model.getFileSystem().directoryAccessible(path);

View file

@ -7,9 +7,7 @@ import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.TerminalHelper; import io.xpipe.app.util.TerminalHelper;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.ProcessControlProvider; import io.xpipe.core.process.*;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.store.*; import io.xpipe.core.store.*;
import io.xpipe.core.util.FailableConsumer; import io.xpipe.core.util.FailableConsumer;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
@ -155,21 +153,25 @@ public final class OpenFileSystemModel {
var name = adjustedPath + " - " + entry.get().getName(); var name = adjustedPath + " - " + entry.get().getName();
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
if (ShellDialects.ALL.stream().anyMatch(dialect -> adjustedPath.startsWith(dialect.getOpenCommand()))) { if (ShellDialects.ALL.stream().anyMatch(dialect -> adjustedPath.startsWith(dialect.getOpenCommand()))) {
TerminalHelper.open(entry.getEntry(), name, fileSystem TerminalHelper.open(
entry.getEntry(),
name,
fileSystem
.getShell() .getShell()
.get() .get()
.subShell(adjustedPath) .subShell(adjustedPath)
.initWith(fileSystem .initWith(new SimpleScriptSnippet(
fileSystem
.getShell() .getShell()
.get() .get()
.getShellDialect() .getShellDialect()
.getCdCommand(currentPath.get()))); .getCdCommand(currentPath.get()),
ScriptSnippet.ExecutionType.BOTH)));
} else { } else {
TerminalHelper.open(entry.getEntry(), name, fileSystem TerminalHelper.open(
.getShell() entry.getEntry(),
.get() name,
.command(adjustedPath) fileSystem.getShell().get().command(adjustedPath).withWorkingDirectory(directory));
.withWorkingDirectory(directory));
} }
}); });
return Optional.ofNullable(currentPath.get()); return Optional.ofNullable(currentPath.get());
@ -363,8 +365,9 @@ public final class OpenFileSystemModel {
var fs = entry.getStore().createFileSystem(); var fs = entry.getStore().createFileSystem();
fs.open(); fs.open();
this.fileSystem = fs; this.fileSystem = fs;
this.local = this.local = fs.getShell()
fs.getShell().map(shellControl -> shellControl.hasLocalSystemAccess()).orElse(false); .map(shellControl -> shellControl.hasLocalSystemAccess())
.orElse(false);
this.cache.init(); this.cache.init();
}); });
} }
@ -393,7 +396,10 @@ public final class OpenFileSystemModel {
var connection = ((ConnectionFileSystem) fileSystem).getShellControl(); var connection = ((ConnectionFileSystem) fileSystem).getShellControl();
var name = directory + " - " + entry.get().getName(); var name = directory + " - " + entry.get().getName();
var toOpen = ProcessControlProvider.get().withDefaultScripts(connection); 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 // Restart connection as we will have to start it anyway, so we speed it up by doing it preemptively
connection.start(); connection.start();

View file

@ -80,14 +80,14 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
vbox.getChildren().add(b.createRegion()); vbox.getChildren().add(b.createRegion());
} }
{ // {
var b = new IconButtonComp("mdi2c-comment-processing-outline", () -> Hyperlinks.open(Hyperlinks.ROADMAP)) // var b = new IconButtonComp("mdi2c-comment-processing-outline", () -> Hyperlinks.open(Hyperlinks.ROADMAP))
.apply(new FancyTooltipAugment<>("roadmap")); // .apply(new FancyTooltipAugment<>("roadmap"));
b.apply(struc -> { // b.apply(struc -> {
AppFont.setSize(struc.get(), 2); // AppFont.setSize(struc.get(), 2);
}); // });
vbox.getChildren().add(b.createRegion()); // vbox.getChildren().add(b.createRegion());
} // }
{ {

View file

@ -345,7 +345,7 @@ public abstract class StoreEntryComp extends SimpleComp {
if (wrapper.getEntry().getProvider() != null && wrapper.getEntry().getProvider().canMoveCategories()) { if (wrapper.getEntry().getProvider() != null && wrapper.getEntry().getProvider().canMoveCategories()) {
var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline")); 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()); MenuItem m = new MenuItem(storeCategoryWrapper.getName());
m.setOnAction(event -> { m.setOnAction(event -> {
wrapper.moveTo(storeCategoryWrapper.getCategory()); wrapper.moveTo(storeCategoryWrapper.getCategory());

View file

@ -46,7 +46,7 @@ public class StoreSection {
if (wrapper != null) { if (wrapper != null) {
this.showDetails = Bindings.createBooleanBinding( this.showDetails = Bindings.createBooleanBinding(
() -> { () -> {
return wrapper.getExpanded().get() || allChildren.size() == 0; return wrapper.getExpanded().get() || allChildren.isEmpty();
}, },
wrapper.getExpanded(), wrapper.getExpanded(),
allChildren); allChildren);
@ -63,20 +63,19 @@ public class StoreSection {
var c = Comparator.<StoreSection>comparingInt( var c = Comparator.<StoreSection>comparingInt(
value -> value.getWrapper().getEntry().getValidity().isUsable() ? -1 : 1); 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( return BindingsHelper.orderedContentBinding(
list, list,
(o1, o2) -> { (o1, o2) -> {
var current = category.getValue(); var current = mappedSortMode.getValue();
if (current != null) { if (current != null) {
return c.thenComparing(current.getSortMode().getValue().comparator()) return c.thenComparing(current.comparator())
.compare(o1, o2); .compare(o1, o2);
} else { } else {
return c.compare(o1, o2); return c.compare(o1, o2);
} }
}, },
category, mappedSortMode);
mapped);
} }
public static StoreSection createTopLevel( public static StoreSection createTopLevel(
@ -118,10 +117,12 @@ public class StoreSection {
} }
var allChildren = BindingsHelper.filteredContentBinding(all, other -> { var allChildren = BindingsHelper.filteredContentBinding(all, other -> {
// Legacy implementation that does not use caches. Use for testing
// if (true) return DataStorage.get() // if (true) return DataStorage.get()
// .getDisplayParent(other.getEntry()) // .getDisplayParent(other.getEntry())
// .map(found -> found.equals(e.getEntry())) // .map(found -> found.equals(e.getEntry()))
// .orElse(false); // .orElse(false);
// This check is fast as the children are cached in the storage // This check is fast as the children are cached in the storage
return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry()); return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry());
}); });
@ -131,8 +132,12 @@ public class StoreSection {
var filtered = BindingsHelper.filteredContentBinding( var filtered = BindingsHelper.filteredContentBinding(
ordered, ordered,
section -> { section -> {
return (filterString == null || section.shouldShow(filterString.get())) var showFilter = filterString == null || section.shouldShow(filterString.get());
&& section.anyMatches(entryFilter); 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, category,
filterString); filterString);

View file

@ -57,6 +57,7 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
struc.get().setAlignment(Pos.CENTER_LEFT); struc.get().setAlignment(Pos.CENTER_LEFT);
}) })
.grow(true, false) .grow(true, false)
.apply(struc -> struc.get().setMnemonicParsing(false))
.styleClass("item"); .styleClass("item");
augment.accept(section, root); augment.accept(section, root);
@ -81,7 +82,6 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
List<Comp<?>> topEntryList = List.of(button, root); List<Comp<?>> topEntryList = List.of(button, root);
list.add(new HorizontalComp(topEntryList) list.add(new HorizontalComp(topEntryList)
.apply(struc -> struc.get().setFillHeight(true))); .apply(struc -> struc.get().setFillHeight(true)));
list.add(Comp.separator().visible(expanded));
} else { } else {
expanded = new SimpleBooleanProperty(true); expanded = new SimpleBooleanProperty(true);
} }

View file

@ -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 // 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()) { if (OsType.getLocal().equals(OsType.MACOS) && AppProperties.get().isDeveloperMode() && AppLogs.get().isWriteToSysout()) {
try { 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) { if (iconUrl != null) {
var awtIcon = ImageIO.read(iconUrl); var awtIcon = ImageIO.read(iconUrl);
Taskbar.getTaskbar().setIconImage(awtIcon); Taskbar.getTaskbar().setIconImage(awtIcon);

View file

@ -174,8 +174,8 @@ public class AppMainWindow {
stage.setWidth(1280); stage.setWidth(1280);
stage.setHeight(720); stage.setHeight(720);
} else { } else {
stage.setX(310); stage.setX(312);
stage.setY(178); stage.setY(149);
stage.setWidth(1296); stage.setWidth(1296);
stage.setHeight(759); stage.setHeight(759);
} }

View file

@ -200,7 +200,7 @@ public class AppTheme {
static Theme getDefaultLightTheme() { static Theme getDefaultLightTheme() {
return switch (OsType.getLocal()) { return switch (OsType.getLocal()) {
case OsType.Windows windows -> PRIMER_LIGHT; case OsType.Windows windows -> PRIMER_LIGHT;
case OsType.Linux linux -> NORD_LIGHT; case OsType.Linux linux -> PRIMER_LIGHT;
case OsType.MacOs macOs -> CUPERTINO_LIGHT; case OsType.MacOs macOs -> CUPERTINO_LIGHT;
}; };
} }
@ -208,7 +208,7 @@ public class AppTheme {
static Theme getDefaultDarkTheme() { static Theme getDefaultDarkTheme() {
return switch (OsType.getLocal()) { return switch (OsType.getLocal()) {
case OsType.Windows windows -> PRIMER_DARK; case OsType.Windows windows -> PRIMER_DARK;
case OsType.Linux linux -> NORD_DARK; case OsType.Linux linux -> PRIMER_DARK;
case OsType.MacOs macOs -> CUPERTINO_DARK; case OsType.MacOs macOs -> CUPERTINO_DARK;
}; };
} }

View file

@ -28,7 +28,7 @@ public class AppTrayIcon {
var image = switch (OsType.getLocal()) { var image = switch (OsType.getLocal()) {
case OsType.Windows windows -> "img/logo/logo_16x16.png"; case OsType.Windows windows -> "img/logo/logo_16x16.png";
case OsType.Linux linux -> "img/logo/logo_24x24.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(); var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, image).orElseThrow();

View file

@ -69,6 +69,7 @@ public class BaseMode extends OperationMode {
DataStorage.reset(); DataStorage.reset();
AppPrefs.reset(); AppPrefs.reset();
AppExtensionManager.reset(); AppExtensionManager.reset();
AppDataLock.unlock();
// Shut down socket server last to keep a non-daemon thread running // Shut down socket server last to keep a non-daemon thread running
AppSocketServer.reset(); AppSocketServer.reset();
TrackEvent.info("mode", "Background mode shutdown finished"); TrackEvent.info("mode", "Background mode shutdown finished");

View file

@ -19,4 +19,8 @@ public class ExtensionException extends RuntimeException {
public ExtensionException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { public ExtensionException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace); super(message, cause, enableSuppression, writableStackTrace);
} }
public static ExtensionException corrupt(String message) {
return new ExtensionException(message + ". Is the installation corrupt?");
}
} }

View file

@ -9,6 +9,7 @@ import io.xpipe.app.core.AppState;
import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.update.XPipeDistributionType; import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.LicenseProvider;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import java.io.*; import java.io.*;
@ -150,6 +151,7 @@ public class SentryErrorHandler implements ErrorHandler {
atts.forEach(attachment -> s.addAttachment(attachment)); 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("updatesEnabled", AppPrefs.get() != null ? AppPrefs.get().automaticallyUpdate().getValue().toString() : "unknown");
s.setTag("initError", String.valueOf(OperationMode.isInStartup())); s.setTag("initError", String.valueOf(OperationMode.isInStartup()));
s.setTag( s.setTag(

View file

@ -359,6 +359,9 @@ public class AppPrefs {
private AppPreferencesFx preferencesFx; private AppPreferencesFx preferencesFx;
private boolean controlsSetup; private boolean controlsSetup;
@Getter
private final Set<Field<?>> proRequiredSettings = new HashSet<>();
private AppPrefs() { private AppPrefs() {
try { try {
preferencesFx = createPreferences(); preferencesFx = createPreferences();

View file

@ -79,7 +79,7 @@ public class CustomFormRenderer extends PreferencesFxFormRenderer {
c.getFieldLabel().setMaxHeight(AppFont.getPixelSize(1)); c.getFieldLabel().setMaxHeight(AppFont.getPixelSize(1));
c.getFieldLabel().textProperty().unbind(); c.getFieldLabel().textProperty().unbind();
c.getFieldLabel().textProperty().bind(Bindings.createStringBinding(() -> { 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())); }, f.labelProperty()));
grid.add(c.getFieldLabel(), 0, i + rowAmount); grid.add(c.getFieldLabel(), 0, i + rowAmount);

View file

@ -177,13 +177,21 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
@Override @Override
protected Optional<Path> determineInstallation() { protected Optional<Path> determineInstallation() {
Optional<String> launcherDir; var perUser = WindowsRegistry.readString(
launcherDir = WindowsRegistry.readString(
WindowsRegistry.HKEY_CURRENT_USER, WindowsRegistry.HKEY_CURRENT_USER,
"SOFTWARE\\71445fac-d6ef-5436-9da7-5a323762d7f5", "SOFTWARE\\71445fac-d6ef-5436-9da7-5a323762d7f5",
"InstallLocation") "InstallLocation")
.map(p -> p + "\\Tabby.exe"); .map(p -> p + "\\Tabby.exe").map(Path::of);
return launcherDir.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;
} }
}; };

View file

@ -18,6 +18,8 @@ import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import java.util.List;
import static io.xpipe.app.prefs.AppPrefs.group; import static io.xpipe.app.prefs.AppPrefs.group;
public class VaultCategory extends AppPrefsCategory { public class VaultCategory extends AppPrefsCategory {
@ -69,6 +71,9 @@ public class VaultCategory extends AppPrefsCategory {
c.setPrefWidth(1000); c.setPrefWidth(1000);
return c; return c;
}); });
if (!pro) {
prefs.getProRequiredSettings().addAll(List.of(enable, remote));
}
return Category.of( return Category.of(
"vault", "vault",
group( group(

View file

@ -7,13 +7,13 @@ import lombok.Getter;
@Getter @Getter
public enum DataStoreColor { public enum DataStoreColor {
@JsonProperty("red") @JsonProperty("red")
RED("red", "\uD83D\uDFE5", Color.BLUE), RED("red", "\uD83D\uDD34", Color.RED),
@JsonProperty("green") @JsonProperty("green")
GREEN("green", "\uD83D\uDFE9", Color.BLUE), GREEN("green", "\uD83D\uDFE2", Color.GREEN),
@JsonProperty("yellow") @JsonProperty("yellow")
YELLOW("yellow", "\uD83D\uDFE8", Color.BLUE), YELLOW("yellow", "\uD83D\uDFE1", Color.YELLOW),
@JsonProperty("blue") @JsonProperty("blue")
BLUE("blue", "\uD83D\uDD35", Color.BLUE); BLUE("blue", "\uD83D\uDD35", Color.BLUE);

View file

@ -1,5 +1,6 @@
package io.xpipe.app.util; package io.xpipe.app.util;
import io.xpipe.app.ext.ExtensionException;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.storage.GitStorageHandler; import io.xpipe.app.storage.GitStorageHandler;
import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellControl;
@ -21,7 +22,7 @@ public abstract class LicenseProvider {
public void init(ModuleLayer layer) { public void init(ModuleLayer layer) {
INSTANCE = ServiceLoader.load(layer, LicenseProvider.class).stream() INSTANCE = ServiceLoader.load(layer, LicenseProvider.class).stream()
.map(ServiceLoader.Provider::get) .map(ServiceLoader.Provider::get)
.findFirst().orElseThrow(); .findFirst().orElseThrow(() -> ExtensionException.corrupt("Missing license provider."));
} }
@Override @Override
@ -35,6 +36,8 @@ public abstract class LicenseProvider {
} }
} }
public abstract boolean hasLicense();
public abstract LicensedFeature getFeature(String id); public abstract LicensedFeature getFeature(String id);
public abstract void handleShellControl(ShellControl sc); public abstract void handleShellControl(ShellControl sc);

View file

@ -41,6 +41,10 @@ public class ScanAlert {
private static void showForShellStore(DataStoreEntry initial) { private static void showForShellStore(DataStoreEntry initial) {
show(initial, (DataStoreEntry entry) -> { show(initial, (DataStoreEntry entry) -> {
try (var sc = ((ShellStore) entry.getStore()).control().start()) { try (var sc = ((ShellStore) entry.getStore()).control().start()) {
if (!sc.getShellDialect().isSupportedShell()) {
return null;
}
var providers = ScanProvider.getAll(); var providers = ScanProvider.getAll();
var applicable = new ArrayList<ScanProvider.ScanOperation>(); var applicable = new ArrayList<ScanProvider.ScanOperation>();
for (ScanProvider scanProvider : providers) { for (ScanProvider scanProvider : providers) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

View file

@ -15,10 +15,8 @@
.bookmark-list .store-section-mini-comp .item:selected { .bookmark-list .store-section-mini-comp .item:selected {
-fx-border-width: 1px; -fx-border-width: 1px;
-fx-border-radius: 4px; -fx-border-radius: 4px;
-fx-border-insets: 0px 10px 0 0; -fx-background-color: transparent, -color-accent-emphasis, -color-bg-overlay;
-fx-border-color: -color-accent-muted;
-fx-background-color: -color-bg-default;
-fx-background-radius: 4px; -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; -fx-padding: 0.25em 0.4em 0.25em 0.4em;
} }

View file

@ -4,6 +4,14 @@
-fx-padding: 1em; -fx-padding: 1em;
} }
.browser .welcome .button {
-fx-border-radius: 4px;
}
.browser .welcome .button:hover {
-fx-background-color: -color-neutral-muted;
}
.browser .tile > * { .browser .tile > * {
-fx-padding: 0.6em 0 0.6em 0; -fx-padding: 0.6em 0 0.6em 0;
} }
@ -163,9 +171,10 @@
} }
.browser .context-menu { .browser .context-menu {
-fx-padding: 0; -fx-padding: 12 0 12 0;
-fx-background-radius: 1px; -fx-background-radius: 8px;
-fx-border-color: -color-neutral-muted; -fx-border-radius: 8px;
-fx-border-color: -color-border-default;
} }
.browser .tab-pane { .browser .tab-pane {

View file

@ -1,6 +1,3 @@
.context-menu {
-fx-background-radius: 5px;
}
.header-menu-item { .header-menu-item {
-fx-background-color: white; -fx-background-color: white;
@ -45,4 +42,6 @@
.context-menu * .context-menu { .context-menu * .context-menu {
-fx-padding: 0; -fx-padding: 0;
-fx-background-radius: 0;
-fx-border-radius: 0;
} }

View file

@ -66,6 +66,10 @@
-fx-background-radius: 0; -fx-background-radius: 0;
} }
.store-entry-comp:hover:armed {
-fx-background-color: derive(-color-neutral-muted, 25%);
}
.store-entry-comp:hover { .store-entry-comp:hover {
-fx-background-color: -color-neutral-muted; -fx-background-color: -color-neutral-muted;
} }
@ -116,11 +120,17 @@
-fx-effect: dropshadow(three-pass-box, -color-shadow-default, 2px, 0.5, 0, 1); -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-border-radius: 4px;
-fx-background-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 { .root.nord .store-entry-section-comp {
-fx-border-radius: 0; -fx-border-radius: 0;
-fx-background-radius: 0; -fx-background-radius: 0;

View file

@ -45,7 +45,7 @@ project.ext {
kebapProductName = isStage ? 'xpipe-ptb' : 'xpipe' kebapProductName = isStage ? 'xpipe-ptb' : 'xpipe'
publisher = 'XPipe UG (haftungsbeschränkt)' publisher = 'XPipe UG (haftungsbeschränkt)'
shortDescription = 'Your entire server infrastructure at your fingertips' 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' website = 'https://xpipe.io'
sourceWebsite = 'https://github.com/xpipe-io/xpipe' sourceWebsite = 'https://github.com/xpipe-io/xpipe'
authors = 'Christopher Schnick' authors = 'Christopher Schnick'

View file

@ -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();
}

View file

@ -15,6 +15,8 @@ import java.util.function.Predicate;
public interface ShellControl extends ProcessControl { public interface ShellControl extends ProcessControl {
List<ScriptSnippet> getInitCommands();
ShellControl withTargetTerminalShellDialect(ShellDialect d); ShellControl withTargetTerminalShellDialect(ShellDialect d);
ShellDialect getTargetTerminalShellDialect(); ShellDialect getTargetTerminalShellDialect();
@ -153,11 +155,7 @@ public interface ShellControl extends ProcessControl {
} }
ShellControl elevationPassword(FailableSupplier<SecretValue> value); ShellControl elevationPassword(FailableSupplier<SecretValue> value);
ShellControl initWith(String cmds); ShellControl initWith(ScriptSnippet snippet);
ShellControl initWithDumb(String cmds);
ShellControl initWithTerminal(String cmds);
ShellControl additionalTimeout(int ms); ShellControl additionalTimeout(int ms);

View file

@ -26,6 +26,10 @@ public interface ShellDialect {
.collect(Collectors.joining(" ")); .collect(Collectors.joining(" "));
} }
default boolean isSupportedShell() {
return true;
}
default boolean isSelectable() { default boolean isSelectable() {
return true; return true;
} }
@ -40,7 +44,7 @@ public interface ShellDialect {
String getCatchAllVariable(); String getCatchAllVariable();
CommandControl queryVersion(ShellControl shellControl); String queryVersion(ShellControl shellControl) throws Exception;
CommandControl prepareUserTempDirectory(ShellControl shellControl, String directory); CommandControl prepareUserTempDirectory(ShellControl shellControl, String directory);

View file

@ -20,6 +20,8 @@ public class ShellDialects {
public static ShellDialect ZSH; public static ShellDialect ZSH;
public static ShellDialect CSH; public static ShellDialect CSH;
public static ShellDialect FISH; public static ShellDialect FISH;
public static ShellDialect UNSUPPORTED;
public static ShellDialect CISCO;
public static class Loader implements ModuleLayerLoader { public static class Loader implements ModuleLayerLoader {
@ -40,6 +42,8 @@ public class ShellDialects {
CSH = byName("csh"); CSH = byName("csh");
ASH = byName("ash"); ASH = byName("ash");
SH = byName("sh"); SH = byName("sh");
UNSUPPORTED = byName("unsupported");
CISCO = byName("cisco");
} }
@Override @Override

View file

@ -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;
}
}

21
dist/changelogs/1.7.3.md vendored Normal file
View file

@ -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

BIN
dist/logo/logo.icns vendored

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 647 B

After

Width:  |  Height:  |  Size: 678 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 117 KiB

View file

@ -1,9 +1,13 @@
package io.xpipe.ext.base.script; package io.xpipe.ext.base.script;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.Validators; import io.xpipe.app.util.Validators;
import io.xpipe.core.process.ScriptSnippet;
import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.SimpleScriptSnippet;
import io.xpipe.core.store.DataStore; import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.DataStoreState; import io.xpipe.core.store.DataStoreState;
import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileNames;
@ -18,8 +22,6 @@ import lombok.extern.jackson.Jacksonized;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects;
import java.util.stream.Collectors;
@SuperBuilder @SuperBuilder
@Getter @Getter
@ -31,56 +33,67 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore,
} }
public static ShellControl controlWithScripts(ShellControl pc, List<DataStoreEntryRef<ScriptStore>> initScripts, List<DataStoreEntryRef<ScriptStore>> bringScripts) { public static ShellControl controlWithScripts(ShellControl pc, List<DataStoreEntryRef<ScriptStore>> initScripts, List<DataStoreEntryRef<ScriptStore>> bringScripts) {
pc.onInit(shellControl -> {
var initFlattened = flatten(initScripts); 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 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); var bringFlattened = flatten(bringScripts);
pc.onInit(shellControl -> {
passInitScripts(pc, initFlattened);
var dir = initScriptsDirectory(shellControl, bringFlattened); var dir = initScriptsDirectory(shellControl, bringFlattened);
if (dir != null) { if (dir != null) {
shellControl.initWithTerminal(shellControl.getShellDialect().appendToPathVariableCommand(dir)); shellControl.initWith(new SimpleScriptSnippet(shellControl.getShellDialect().appendToPathVariableCommand(dir), ScriptSnippet.ExecutionType.TERMINAL_ONLY));
} }
}); });
return pc; return pc;
} }
private static void passInitScripts(ShellControl pc, List<SimpleScriptStore> 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<SimpleScriptStore> scriptStores) throws Exception { private static String initScriptsDirectory(ShellControl proc, List<SimpleScriptStore> scriptStores) throws Exception {
if (scriptStores.size() == 0) { if (scriptStores.isEmpty()) {
return null; 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().<SimpleScriptStore>ref(); return DataStorage.get().getStoreEntries().stream().filter(dataStoreEntry -> dataStoreEntry.getStore() == scriptStore).findFirst().orElseThrow().<SimpleScriptStore>ref();
}).toList(); }).toList();
var hash = refs.stream().mapToInt(value -> value.get().getName().hashCode() + value.getStore().hashCode()).sum(); var hash = refs.stream().mapToInt(value -> value.get().getName().hashCode() + value.getStore().hashCode()).sum();
var xpipeHome = XPipeInstallation.getDataDir(proc); 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 hashFile = FileNames.join(targetDir, "hash");
var d = proc.getShellDialect(); var d = proc.getShellDialect();
if (d.createFileExistsCommand(proc, hashFile).executeAndCheck()) { if (d.createFileExistsCommand(proc, hashFile).executeAndCheck()) {
var read = d.getFileReadCommand(proc, hashFile).readStdoutOrThrow(); var read = d.getFileReadCommand(proc, hashFile).readStdoutOrThrow();
try {
var readHash = Integer.parseInt(read); var readHash = Integer.parseInt(read);
if (hash == readHash) { if (hash == readHash) {
return targetDir; return targetDir;
} }
} catch (NumberFormatException e) {
ErrorEvent.fromThrowable(e).omit().handle();
}
} }
if (d.directoryExists(proc, targetDir).executeAndCheck()) {
d.deleteFileOrDirectory(proc, targetDir).execute(); d.deleteFileOrDirectory(proc, targetDir).execute();
}
proc.executeSimpleCommand(d.getMkdirsCommand(targetDir)); proc.executeSimpleCommand(d.getMkdirsCommand(targetDir));
for (DataStoreEntryRef<SimpleScriptStore> scriptStore : refs) { for (DataStoreEntryRef<SimpleScriptStore> scriptStore : refs) {
@ -90,8 +103,10 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore,
d.createScriptTextFileWriteCommand(proc, content, scriptFile).execute(); d.createScriptTextFileWriteCommand(proc, content, scriptFile).execute();
var chmod = d.getScriptPermissionsCommand(scriptFile); var chmod = d.getScriptPermissionsCommand(scriptFile);
if (chmod != null) {
proc.executeSimpleBooleanCommand(chmod); proc.executeSimpleBooleanCommand(chmod);
} }
}
d.createTextFileWriteCommand(proc, String.valueOf(hash), hashFile).execute(); d.createTextFileWriteCommand(proc, String.valueOf(hash), hashFile).execute();
return targetDir; return targetDir;
@ -101,7 +116,7 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore,
return DataStorage.get().getStoreEntries().stream() return DataStorage.get().getStoreEntries().stream()
.filter(dataStoreEntry -> dataStoreEntry.getStore() instanceof ScriptStore scriptStore .filter(dataStoreEntry -> dataStoreEntry.getStore() instanceof ScriptStore scriptStore
&& scriptStore.getState().isDefault()) && scriptStore.getState().isDefault())
.map(e -> e.<ScriptStore>ref()) .map(DataStoreEntry::<ScriptStore>ref)
.toList(); .toList();
} }
@ -109,7 +124,7 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore,
return DataStorage.get().getStoreEntries().stream() return DataStorage.get().getStoreEntries().stream()
.filter(dataStoreEntry -> dataStoreEntry.getStore() instanceof ScriptStore scriptStore .filter(dataStoreEntry -> dataStoreEntry.getStore() instanceof ScriptStore scriptStore
&& scriptStore.getState().isBringToShell()) && scriptStore.getState().isBringToShell())
.map(e -> e.<ScriptStore>ref()) .map(DataStoreEntry::<ScriptStore>ref)
.toList(); .toList();
} }
@ -155,12 +170,6 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore,
// } // }
} }
public LinkedHashSet<SimpleScriptStore> getFlattenedScripts() {
var set = new LinkedHashSet<SimpleScriptStore>();
queryFlattenedScripts(set);
return set;
}
protected abstract void queryFlattenedScripts(LinkedHashSet<SimpleScriptStore> all); protected abstract void queryFlattenedScripts(LinkedHashSet<SimpleScriptStore> all);
public abstract List<DataStoreEntryRef<ScriptStore>> getEffectiveScripts(); public abstract List<DataStoreEntryRef<ScriptStore>> getEffectiveScripts();

View file

@ -1,10 +1,10 @@
package io.xpipe.ext.base.script; package io.xpipe.ext.base.script;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.ScriptHelper; import io.xpipe.app.util.ScriptHelper;
import io.xpipe.app.util.Validators; import io.xpipe.app.util.Validators;
import io.xpipe.core.process.ScriptSnippet;
import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialect; import io.xpipe.core.process.ShellDialect;
import lombok.Getter; import lombok.Getter;
@ -13,21 +13,14 @@ import lombok.extern.jackson.Jacksonized;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@SuperBuilder @SuperBuilder
@Getter @Getter
@Jacksonized @Jacksonized
@JsonTypeName("script") @JsonTypeName("script")
public class SimpleScriptStore extends ScriptStore { public class SimpleScriptStore extends ScriptStore implements ScriptSnippet {
public String prepareDumbScript(ShellControl shellControl) {
return assemble(shellControl, ExecutionType.DUMB_ONLY);
}
public String prepareTerminalScript(ShellControl shellControl) {
return assemble(shellControl, ExecutionType.TERMINAL_ONLY);
}
private String assemble(ShellControl shellControl, ExecutionType type) { private String assemble(ShellControl shellControl, ExecutionType type) {
var targetType = type == ExecutionType.TERMINAL_ONLY var targetType = type == ExecutionType.TERMINAL_ONLY
@ -51,39 +44,35 @@ public class SimpleScriptStore extends ScriptStore {
@Override @Override
public List<DataStoreEntryRef<ScriptStore>> getEffectiveScripts() { public List<DataStoreEntryRef<ScriptStore>> getEffectiveScripts() {
return scripts != null return scripts != null
? scripts.stream().filter(scriptStore -> scriptStore != null).toList() ? scripts.stream().filter(Objects::nonNull).toList()
: List.of(); : List.of();
} }
public void queryFlattenedScripts(LinkedHashSet<SimpleScriptStore> all) { public void queryFlattenedScripts(LinkedHashSet<SimpleScriptStore> all) {
// Prevent loop
all.add(this); all.add(this);
getEffectiveScripts().stream() getEffectiveScripts().stream()
.filter(scriptStoreDataStoreEntryRef -> !all.contains(scriptStoreDataStoreEntryRef.getStore())) .filter(scriptStoreDataStoreEntryRef -> !all.contains(scriptStoreDataStoreEntryRef.getStore()))
.forEach(scriptStoreDataStoreEntryRef -> { .forEach(scriptStoreDataStoreEntryRef -> {
scriptStoreDataStoreEntryRef.getStore().queryFlattenedScripts(all); scriptStoreDataStoreEntryRef.getStore().queryFlattenedScripts(all);
}); });
all.remove(this);
all.add(this);
} }
@Getter @Override
public enum ExecutionType { public String content(ShellControl shellControl) {
@JsonProperty("dumbOnly") return assemble(shellControl, executionType);
DUMB_ONLY("dumbOnly"),
@JsonProperty("terminalOnly")
TERMINAL_ONLY("terminalOnly"),
@JsonProperty("both")
BOTH("both");
private final String id;
ExecutionType(String id) {
this.id = id;
} }
@Override
public ScriptSnippet.ExecutionType executionType() {
return executionType;
} }
private final ShellDialect minimumDialect; private final ShellDialect minimumDialect;
private final String commands; private final String commands;
private final ExecutionType executionType; private final ExecutionType executionType;
private final boolean requiresElevation;
@Override @Override
public void checkComplete() throws Exception { public void checkComplete() throws Exception {

View file

@ -137,7 +137,6 @@ public class SimpleScriptStoreProvider implements DataStoreProvider {
new SimpleListProperty<>(FXCollections.observableArrayList(new ArrayList<>(st.getEffectiveScripts()))); new SimpleListProperty<>(FXCollections.observableArrayList(new ArrayList<>(st.getEffectiveScripts())));
Property<String> commandProp = new SimpleObjectProperty<>(st.getCommands()); Property<String> commandProp = new SimpleObjectProperty<>(st.getCommands());
var type = new SimpleObjectProperty<>(st.getExecutionType()); var type = new SimpleObjectProperty<>(st.getExecutionType());
var requiresElevationProperty = new SimpleBooleanProperty(st.isRequiresElevation());
Comp<?> choice = (Comp<?>) Class.forName( Comp<?> choice = (Comp<?>) Class.forName(
AppExtensionManager.getInstance() AppExtensionManager.getInstance()
@ -150,6 +149,7 @@ public class SimpleScriptStoreProvider implements DataStoreProvider {
return new OptionsBuilder() return new OptionsBuilder()
.name("snippets") .name("snippets")
.description("snippetsDescription") .description("snippetsDescription")
.longDescription("base:scriptDependencies")
.addComp( .addComp(
new DataStoreListChoiceComp<>( new DataStoreListChoiceComp<>(
others, others,
@ -159,6 +159,7 @@ public class SimpleScriptStoreProvider implements DataStoreProvider {
others) others)
.name("minimumShellDialect") .name("minimumShellDialect")
.description("minimumShellDialectDescription") .description("minimumShellDialectDescription")
.longDescription("base:scriptCompatibility")
.addComp(choice, dialect) .addComp(choice, dialect)
.nonNull() .nonNull()
.name("scriptContents") .name("scriptContents")
@ -175,10 +176,6 @@ public class SimpleScriptStoreProvider implements DataStoreProvider {
.description("executionTypeDescription") .description("executionTypeDescription")
.longDescription("base:executionType") .longDescription("base:executionType")
.addComp(new ScriptStoreTypeChoiceComp(type), type) .addComp(new ScriptStoreTypeChoiceComp(type), type)
.name("shouldElevate")
.description("shouldElevateDescription")
.longDescription("proc:elevation")
.addToggle(requiresElevationProperty)
.name("scriptGroup") .name("scriptGroup")
.description("scriptGroupDescription") .description("scriptGroupDescription")
.addComp( .addComp(
@ -195,7 +192,6 @@ public class SimpleScriptStoreProvider implements DataStoreProvider {
.description(st.getDescription()) .description(st.getDescription())
.commands(commandProp.getValue()) .commands(commandProp.getValue())
.executionType(type.get()) .executionType(type.get())
.requiresElevation(requiresElevationProperty.get())
.build(); .build();
}, },
store) store)
@ -210,8 +206,7 @@ public class SimpleScriptStoreProvider implements DataStoreProvider {
@Override @Override
public ObservableValue<String> informationString(StoreEntryWrapper wrapper) { public ObservableValue<String> informationString(StoreEntryWrapper wrapper) {
SimpleScriptStore scriptStore = wrapper.getEntry().getStore().asNeeded(); SimpleScriptStore scriptStore = wrapper.getEntry().getStore().asNeeded();
return new SimpleStringProperty((scriptStore.isRequiresElevation() ? "Elevated " : "") return new SimpleStringProperty((scriptStore.getMinimumDialect() != null
+ (scriptStore.getMinimumDialect() != null
? scriptStore.getMinimumDialect().getDisplayName() + " " ? scriptStore.getMinimumDialect().getDisplayName() + " "
: "") : "")
+ (scriptStore.getExecutionType() == SimpleScriptStore.ExecutionType.TERMINAL_ONLY + (scriptStore.getExecutionType() == SimpleScriptStore.ExecutionType.TERMINAL_ONLY
@ -273,7 +268,6 @@ public class SimpleScriptStoreProvider implements DataStoreProvider {
return SimpleScriptStore.builder() return SimpleScriptStore.builder()
.scripts(List.of()) .scripts(List.of())
.executionType(SimpleScriptStore.ExecutionType.TERMINAL_ONLY) .executionType(SimpleScriptStore.ExecutionType.TERMINAL_ONLY)
.requiresElevation(false)
.build(); .build();
} }

View file

@ -1,14 +1,17 @@
## Execution types ## Execution types
There are two distinct execution phases when XPipe connects to a system. There are two distinct execution types 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.
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. The first connection to a system is made in the background in a dumb terminal.
### Blocking commands
Blocking commands that require user input can freeze the shell process when XPipe starts it up internally first in the background. 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. 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.

View file

@ -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.

View file

@ -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.

View file

@ -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. You don't have to specify a shebang line for shells that support it, one is added automatically with the appropriate shell type.

View file

@ -72,9 +72,9 @@ scriptContentsDescription=The script commands to execute
snippets=Script dependencies snippets=Script dependencies
snippetsDescription=Other scripts to run first snippetsDescription=Other scripts to run first
snippetsDependenciesDescription=All possible scripts that should be run if applicable 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 bringToShells=Bring to all compatible shells
isDefaultGroup=Enable all group scripts isDefaultGroup=Run all group scripts on shell init
executionType=Execution type executionType=Execution type
executionTypeDescription=When to run this snippet executionTypeDescription=When to run this snippet
minimumShellDialect=Shell type minimumShellDialect=Shell type

View file

@ -1 +1 @@
1.7.2 1.7.3-3