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/
build/
.idea
.idea/*
!.idea/codeStyles
!.idea/inspectionProfiles
lib/
dev.properties
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"/>
### 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:
<br>
<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>
<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
@ -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

View file

@ -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<StoreEntryWrapper> 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 -> {
var top = new HorizontalComp(List.of(category, filter.hgrow())).styleClass("categories").apply(struc -> {
AppFont.medium(struc.get());
struc.get().setFillHeight(true);
})
.createRegion();
}).createRegion();
var r = section.vgrow().createRegion();
var content = new VBox(top, r);
content.setFillWidth(true);

View file

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

View file

@ -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<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) {
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) {
state.getLastSystems().forEach(e -> {
restoreState(e, null);
@ -87,8 +62,7 @@ public class BrowserModel {
var list = new ArrayList<BrowserSavedState.Entry>();
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);
}
@ -141,9 +112,7 @@ public class BrowserModel {
}
public void openExistingFileSystemIfPresent(DataStoreEntryRef<? extends FileSystemStore> store) {
var found = openFileSystems.stream()
.filter(model -> Objects.equals(model.getEntry(), store))
.findFirst();
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;
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
// Prevent multiple calls from interfering with each other
synchronized (BrowserModel.this) {
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
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;
}
}
}

View file

@ -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());
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);
});
storeList.getChildren().add(tile);
});
var sp = new ScrollPane(storeList);
sp.setFitToWidth(true);
}).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 -> {

View file

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

View file

@ -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
TerminalHelper.open(
entry.getEntry(),
name,
fileSystem
.getShell()
.get()
.subShell(adjustedPath)
.initWith(fileSystem
.initWith(new SimpleScriptSnippet(
fileSystem
.getShell()
.get()
.getShellDialect()
.getCdCommand(currentPath.get())));
.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();

View file

@ -80,14 +80,14 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
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());
// }
{

View file

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

View file

@ -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.<StoreSection>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);

View file

@ -57,6 +57,7 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
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<CompStructure<VBox>> {
List<Comp<?>> 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);
}

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
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);

View file

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

View file

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

View file

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

View file

@ -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");

View file

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

View file

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

View file

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

View file

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

View file

@ -177,13 +177,21 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
@Override
protected Optional<Path> determineInstallation() {
Optional<String> 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;
}
};

View file

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

View file

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

View file

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

View file

@ -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<ScanProvider.ScanOperation>();
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 {
-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;
}

View file

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

View file

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

View file

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

View file

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

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 {
List<ScriptSnippet> getInitCommands();
ShellControl withTargetTerminalShellDialect(ShellDialect d);
ShellDialect getTargetTerminalShellDialect();
@ -153,11 +155,7 @@ public interface ShellControl extends ProcessControl {
}
ShellControl elevationPassword(FailableSupplier<SecretValue> value);
ShellControl initWith(String cmds);
ShellControl initWithDumb(String cmds);
ShellControl initWithTerminal(String cmds);
ShellControl initWith(ScriptSnippet snippet);
ShellControl additionalTimeout(int ms);

View file

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

View file

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

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;
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<DataStoreEntryRef<ScriptStore>> initScripts, List<DataStoreEntryRef<ScriptStore>> 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 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);
pc.onInit(shellControl -> {
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<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 {
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().<SimpleScriptStore>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();
try {
var readHash = Integer.parseInt(read);
if (hash == readHash) {
return targetDir;
}
} catch (NumberFormatException e) {
ErrorEvent.fromThrowable(e).omit().handle();
}
}
if (d.directoryExists(proc, targetDir).executeAndCheck()) {
d.deleteFileOrDirectory(proc, targetDir).execute();
}
proc.executeSimpleCommand(d.getMkdirsCommand(targetDir));
for (DataStoreEntryRef<SimpleScriptStore> scriptStore : refs) {
@ -90,8 +103,10 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore,
d.createScriptTextFileWriteCommand(proc, content, scriptFile).execute();
var chmod = d.getScriptPermissionsCommand(scriptFile);
if (chmod != null) {
proc.executeSimpleBooleanCommand(chmod);
}
}
d.createTextFileWriteCommand(proc, String.valueOf(hash), hashFile).execute();
return targetDir;
@ -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.<ScriptStore>ref())
.map(DataStoreEntry::<ScriptStore>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.<ScriptStore>ref())
.map(DataStoreEntry::<ScriptStore>ref)
.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);
public abstract List<DataStoreEntryRef<ScriptStore>> getEffectiveScripts();

View file

@ -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<DataStoreEntryRef<ScriptStore>> getEffectiveScripts() {
return scripts != null
? scripts.stream().filter(scriptStore -> scriptStore != null).toList()
? scripts.stream().filter(Objects::nonNull).toList()
: List.of();
}
public void queryFlattenedScripts(LinkedHashSet<SimpleScriptStore> 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");
private final String id;
ExecutionType(String id) {
this.id = id;
@Override
public String content(ShellControl shellControl) {
return assemble(shellControl, executionType);
}
@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 {

View file

@ -137,7 +137,6 @@ public class SimpleScriptStoreProvider implements DataStoreProvider {
new SimpleListProperty<>(FXCollections.observableArrayList(new ArrayList<>(st.getEffectiveScripts())));
Property<String> 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<String> 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();
}

View file

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

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.

View file

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

View file

@ -1 +1 @@
1.7.2
1.7.3-3