Merge branch 1.7.3 into master
4
.gitignore
vendored
|
@ -1,6 +1,8 @@
|
|||
.gradle/
|
||||
build/
|
||||
.idea
|
||||
.idea/*
|
||||
!.idea/codeStyles
|
||||
!.idea/inspectionProfiles
|
||||
lib/
|
||||
dev.properties
|
||||
extensions.txt
|
||||
|
|
33
README.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
// }
|
||||
|
||||
|
||||
{
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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?");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 468 B |
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -66,6 +66,10 @@
|
|||
-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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
|
|
@ -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'
|
||||
|
|
35
core/src/main/java/io/xpipe/core/process/ScriptSnippet.java
Normal 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();
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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
BIN
dist/logo/logo.iconset/icon_128x128.png
vendored
Before Width: | Height: | Size: 7 KiB After Width: | Height: | Size: 7.6 KiB |
BIN
dist/logo/logo.iconset/icon_128x128@2.png
vendored
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 20 KiB |
BIN
dist/logo/logo.iconset/icon_16x16.png
vendored
Before Width: | Height: | Size: 647 B After Width: | Height: | Size: 678 B |
BIN
dist/logo/logo.iconset/icon_16x16@2.png
vendored
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
BIN
dist/logo/logo.iconset/icon_256x256.png
vendored
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 20 KiB |
BIN
dist/logo/logo.iconset/icon_256x256@2.png
vendored
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 48 KiB |
BIN
dist/logo/logo.iconset/icon_32x32.png
vendored
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
BIN
dist/logo/logo.iconset/icon_32x32@2.png
vendored
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.2 KiB |
BIN
dist/logo/logo.iconset/icon_512x512.png
vendored
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 48 KiB |
BIN
dist/logo/logo.iconset/icon_512x512@2.png
vendored
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 117 KiB |
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
2
version
|
@ -1 +1 @@
|
|||
1.7.2
|
||||
1.7.3-3
|