Dock rework

This commit is contained in:
crschnick
2024-11-04 22:12:40 +00:00
parent 55b6328516
commit 82d2d48e8a
44 changed files with 936 additions and 167 deletions

View File

@@ -2,12 +2,14 @@ package io.xpipe.app.beacon.impl;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.app.terminal.TerminalLauncher;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionTerminalExchange;
import com.sun.net.httpserver.HttpExchange;
import java.util.UUID;
public class ConnectionTerminalExchangeImpl extends ConnectionTerminalExchange {
@Override

View File

@@ -1,7 +1,7 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.util.TerminalLauncherManager;
import io.xpipe.app.terminal.TerminalLauncherManager;
import io.xpipe.beacon.api.SshLaunchExchange;
import io.xpipe.core.process.ShellDialects;

View File

@@ -1,6 +1,6 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.util.TerminalLauncherManager;
import io.xpipe.app.terminal.TerminalLauncherManager;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.TerminalLaunchExchange;

View File

@@ -1,6 +1,7 @@
package io.xpipe.app.beacon.impl;
import io.xpipe.app.util.TerminalLauncherManager;
import io.xpipe.app.terminal.TerminalLauncherManager;
import io.xpipe.app.terminal.TerminalView;
import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.BeaconServerException;
import io.xpipe.beacon.api.TerminalWaitExchange;
@@ -11,6 +12,7 @@ public class TerminalWaitExchangeImpl extends TerminalWaitExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException, BeaconServerException {
TerminalLauncherManager.waitExchange(msg.getRequest());
TerminalView.get().open(msg.getRequest(), msg.getPid());
return Response.builder().build();
}

View File

@@ -6,13 +6,10 @@ import io.xpipe.app.browser.session.BrowserSessionTab;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.storage.DataColor;
import io.xpipe.core.store.*;
import javafx.beans.property.*;
public final class BrowserHomeTabModel extends BrowserSessionTab {
public final class BrowserHomeModel extends BrowserSessionTab {
public BrowserHomeModel(BrowserAbstractSessionModel<?> browserModel) {
public BrowserHomeTabModel(BrowserAbstractSessionModel<?> browserModel) {
super(browserModel, AppI18n.get("overview"), null);
}

View File

@@ -0,0 +1,89 @@
package io.xpipe.app.browser;
import io.xpipe.app.browser.session.BrowserAbstractSessionModel;
import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.browser.session.BrowserSessionTab;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.terminal.TerminalDockComp;
import io.xpipe.app.terminal.TerminalDockModel;
import io.xpipe.app.terminal.TerminalView;
import io.xpipe.app.terminal.TerminalViewInstance;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.util.ArrayList;
import java.util.UUID;
public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
private final ObservableList<UUID> terminalRequests;
private final TerminalDockModel dockModel = new TerminalDockModel();
public BrowserTerminalDockTabModel(BrowserAbstractSessionModel<?> browserModel, ObservableList<UUID> terminalRequests) {
super(browserModel, AppI18n.get("terminal"), null);
this.terminalRequests = terminalRequests;
}
@Override
public Comp<?> comp() {
return new TerminalDockComp(dockModel);
}
@Override
public boolean canImmediatelyClose() {
return true;
}
@Override
public void init() throws Exception {
var terminals = new ArrayList<TerminalViewInstance>();
TerminalView.get().addListener(new TerminalView.Listener() {
@Override
public void onSessionOpened(TerminalView.Session session) {
if (!terminalRequests.contains(session.getRequest())) {
return;
}
var tv = terminals.stream().filter(instance -> instance.getTerminalProcess().equals(session.getTerminal())).findFirst();
tv.ifPresent(instance -> {
dockModel.trackTerminal(instance);
});
}
@Override
public void onSessionClosed(TerminalView.Session session) {
}
@Override
public void onTerminalOpened(TerminalViewInstance instance) {
terminals.add(instance);
}
@Override
public void onTerminalClosed(TerminalViewInstance instance) {
terminals.remove(instance);
}
});
}
@Override
public void close() {}
@Override
public String getIcon() {
return null;
}
@Override
public DataColor getColor() {
return null;
}
@Override
public boolean isCloseable() {
return false;
}
}

View File

@@ -18,7 +18,7 @@ import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.app.terminal.TerminalLauncher;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.ShellControl;
@@ -28,9 +28,12 @@ import io.xpipe.core.store.*;
import io.xpipe.core.util.FailableConsumer;
import io.xpipe.core.util.FailableRunnable;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
import lombok.SneakyThrows;
@@ -39,6 +42,7 @@ import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Stream;
@Getter
@@ -51,6 +55,7 @@ public final class OpenFileSystemModel extends BrowserStoreSessionTab<FileSystem
private final Property<ModalOverlayComp.OverlayContent> overlay = new SimpleObjectProperty<>();
private final BooleanProperty inOverview = new SimpleBooleanProperty();
private final Property<BrowserTransferProgress> progress = new SimpleObjectProperty<>();
private final ObservableList<UUID> terminalRequests = FXCollections.observableArrayList();
private FileSystem fileSystem;
private OpenFileSystemSavedState savedState;
private OpenFileSystemCache cache;
@@ -253,6 +258,8 @@ public final class OpenFileSystemModel extends BrowserStoreSessionTab<FileSystem
if (ShellDialects.getStartableDialects().stream().anyMatch(dialect -> adjustedPath
.toLowerCase()
.startsWith(dialect.getExecutableName().toLowerCase()))) {
var uuid = UUID.randomUUID();
terminalRequests.add(uuid);
TerminalLauncher.open(
entry.getEntry(),
name,
@@ -261,13 +268,17 @@ public final class OpenFileSystemModel extends BrowserStoreSessionTab<FileSystem
.getShell()
.get()
.singularSubShell(
ShellOpenFunction.of(CommandBuilder.ofString(adjustedPath), false)));
ShellOpenFunction.of(CommandBuilder.ofString(adjustedPath), false)),
uuid);
} else {
var uuid = UUID.randomUUID();
terminalRequests.add(uuid);
TerminalLauncher.open(
entry.getEntry(),
name,
directory,
fileSystem.getShell().get().command(adjustedPath));
fileSystem.getShell().get().command(adjustedPath),
uuid);
}
});
return Optional.ofNullable(currentPath.get());
@@ -533,7 +544,9 @@ public final class OpenFileSystemModel extends BrowserStoreSessionTab<FileSystem
var connection = fileSystem.getShell().get();
var name = (directory != null ? directory + " - " : "")
+ entry.get().getName();
TerminalLauncher.open(entry.getEntry(), name, directory, connection);
var uuid = UUID.randomUUID();
terminalRequests.add(uuid);
TerminalLauncher.open(entry.getEntry(), name, directory, connection, uuid);
// Restart connection as we will have to start it anyway, so we speed it up by doing it preemptively
startIfNeeded();

View File

@@ -122,7 +122,7 @@ public class BrowserSessionComp extends SimpleComp {
});
var loadingIndicator = LoadingOverlayComp.noProgress(Comp.empty(), model.getBusy())
.apply(struc -> {
AnchorPane.setTopAnchor(struc.get(), 0.0);
AnchorPane.setTopAnchor(struc.get(), 3.0);
AnchorPane.setRightAnchor(struc.get(), 0.0);
})
.styleClass("tab-loading-indicator");

View File

@@ -1,6 +1,6 @@
package io.xpipe.app.browser.session;
import io.xpipe.app.browser.BrowserHomeModel;
import io.xpipe.app.browser.BrowserHomeTabModel;
import io.xpipe.app.browser.BrowserSavedState;
import io.xpipe.app.browser.BrowserSavedStateImpl;
import io.xpipe.app.browser.BrowserTransferModel;
@@ -27,7 +27,7 @@ public class BrowserSessionModel extends BrowserAbstractSessionModel<BrowserSess
public static final BrowserSessionModel DEFAULT = new BrowserSessionModel();
static {
DEFAULT.getSessionEntries().add(new BrowserHomeModel(DEFAULT));
DEFAULT.getSessionEntries().add(new BrowserHomeTabModel(DEFAULT));
}
private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this);

View File

@@ -11,6 +11,7 @@ import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.terminal.TerminalView;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue;
import javafx.scene.Parent;
@@ -49,14 +50,16 @@ public class AppLayoutComp extends Comp<CompStructure<Pane>> {
var sidebarR = sidebar.createRegion();
pane.setRight(sidebarR);
model.getSelected().addListener((c, o, n) -> {
if (o != null && o.equals(model.getEntries().get(2))) {
if (o != null && o.equals(model.getEntries().get(3))) {
AppPrefs.get().save();
DataStorage.get().saveAsync();
}
if (o != null && o.equals(model.getEntries().get(1))) {
if (o != null && o.equals(model.getEntries().get(0))) {
StoreViewState.get().updateDisplay();
}
// TerminalView.get().toggleView(model.getEntries().get(2).equals(n));
});
pane.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
sidebarR.getChildrenUnmodifiable().forEach(node -> {

View File

@@ -3,6 +3,7 @@ package io.xpipe.app.core;
import io.xpipe.app.beacon.AppBeaconServer;
import io.xpipe.app.browser.session.BrowserSessionComp;
import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.terminal.TerminalDockComp;
import io.xpipe.app.comp.store.StoreLayoutComp;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.util.LabelGraphic;
@@ -10,6 +11,7 @@ import io.xpipe.app.prefs.AppPrefsComp;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.LicenseProvider;
import io.xpipe.app.terminal.TerminalView;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
@@ -22,7 +24,6 @@ import lombok.Data;
import lombok.Getter;
import lombok.extern.jackson.Jacksonized;
import java.time.*;
import java.util.ArrayList;
import java.util.List;
@@ -40,7 +41,7 @@ public class AppLayoutModel {
public AppLayoutModel(SavedState savedState) {
this.savedState = savedState;
this.entries = createEntryList();
this.selected = new SimpleObjectProperty<>(entries.get(1));
this.selected = new SimpleObjectProperty<>(entries.get(0));
}
public static AppLayoutModel get() {
@@ -62,35 +63,49 @@ public class AppLayoutModel {
}
public void selectBrowser() {
selected.setValue(entries.getFirst());
selected.setValue(entries.get(1));
}
public void selectSettings() {
public void selectTerminal() {
if (!TerminalView.isSupported()) {
return;
}
selected.setValue(entries.get(2));
}
public void selectSettings() {
selected.setValue(entries.get(TerminalView.isSupported() ? 3 : 2));
}
public void selectLicense() {
selected.setValue(entries.get(3));
selected.setValue(entries.get(TerminalView.isSupported() ? 4 : 3));
}
public void selectConnections() {
selected.setValue(entries.get(1));
selected.setValue(entries.get(0));
}
private List<Entry> createEntryList() {
var l = new ArrayList<>(List.of(
new Entry(
AppI18n.observable("browser"),
new LabelGraphic.IconGraphic("mdi2f-file-cabinet"),
new BrowserSessionComp(BrowserSessionModel.DEFAULT),
null,
new KeyCodeCombination(KeyCode.DIGIT1, KeyCombination.SHORTCUT_DOWN)),
new Entry(
AppI18n.observable("connections"),
new LabelGraphic.IconGraphic("mdi2c-connection"),
new StoreLayoutComp(),
null,
new KeyCodeCombination(KeyCode.DIGIT1, KeyCombination.SHORTCUT_DOWN)),
new Entry(
AppI18n.observable("browser"),
new LabelGraphic.IconGraphic("mdi2f-file-cabinet"),
new BrowserSessionComp(BrowserSessionModel.DEFAULT),
null,
new KeyCodeCombination(KeyCode.DIGIT2, KeyCombination.SHORTCUT_DOWN)),
// new Entry(
// AppI18n.observable("terminal"),
// new LabelGraphic.IconGraphic("mdi2m-monitor-screenshot"),
// new TerminalDockComp(),
// null,
// new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.SHORTCUT_DOWN)),
new Entry(
AppI18n.observable("settings"),
new LabelGraphic.IconGraphic("mdsmz-miscellaneous_services"),
@@ -129,6 +144,11 @@ public class AppLayoutModel {
// () -> Hyperlinks.open(Hyperlinks.GITHUB_WEBTOP),
// null)
));
if (!TerminalView.isSupported()) {
l.remove(2);
}
return l;
}

View File

@@ -15,6 +15,7 @@ import io.xpipe.app.resources.AppResources;
import io.xpipe.app.resources.SystemIcons;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStorageSyncHandler;
import io.xpipe.app.terminal.TerminalView;
import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.*;
@@ -67,6 +68,7 @@ public class BaseMode extends OperationMode {
FileBridge.init();
BlobManager.init();
ActionProvider.initProviders();
TerminalView.init();
TrackEvent.info("Finished base components initialization");
initialized = true;
}

View File

@@ -264,6 +264,9 @@ public class AppMainWindow {
public void show() {
stage.show();
if (OsType.getLocal() == OsType.WINDOWS) {
NativeWinWindowControl.MAIN_WINDOW = new NativeWinWindowControl(stage);
}
}
private void setupContent(Comp<?> content) {

View File

@@ -1,5 +1,8 @@
package io.xpipe.app.core.window;
import com.sun.jna.ptr.IntByReference;
import io.sentry.protocol.User;
import io.xpipe.app.util.Rect;
import javafx.stage.Window;
import com.sun.jna.Library;
@@ -13,10 +16,34 @@ import lombok.Getter;
import lombok.SneakyThrows;
import java.lang.reflect.Method;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
@Getter
public class NativeWinWindowControl {
public static Optional<NativeWinWindowControl> byPid(long pid) {
var ref = new AtomicReference<NativeWinWindowControl>();
User32.INSTANCE.EnumWindows((hWnd, data) -> {
var visible = User32.INSTANCE.IsWindowVisible(hWnd);
if (!visible) {
return true;
}
var wpid = new IntByReference();
User32.INSTANCE.GetWindowThreadProcessId(hWnd, wpid);
if (wpid.getValue() == pid) {
ref.set(new NativeWinWindowControl(hWnd));
return false;
} else {
return true;
}
}, null);
return Optional.ofNullable(ref.get());
}
public static NativeWinWindowControl MAIN_WINDOW;
private final WinDef.HWND windowHandle;
@SneakyThrows
@@ -38,8 +65,48 @@ public class NativeWinWindowControl {
this.windowHandle = windowHandle;
}
public void move(int x, int y, int w, int h) {
User32.INSTANCE.SetWindowPos(windowHandle, new WinDef.HWND(), x, y, w, h, 0);
public void removeBorders() {
var style = User32.INSTANCE.GetWindowLong(windowHandle, User32.GWL_STYLE);
var mod = style & ~(User32.WS_CAPTION | User32.WS_THICKFRAME | User32.WS_MAXIMIZEBOX);
User32.INSTANCE.SetWindowLong(windowHandle,User32.GWL_STYLE,mod);
}
public boolean isIconified() {
return (User32.INSTANCE.GetWindowLong(windowHandle,User32.GWL_STYLE) & User32.WS_MINIMIZE) != 0;
}
public void alwaysInFront() {
orderRelative(new WinDef.HWND(new Pointer( 0xFFFFFFFFFFFFFFFFL)));
}
public void defaultOrder() {
orderRelative(new WinDef.HWND(new Pointer( -2)));
}
public void orderRelative(WinDef.HWND predecessor) {
User32.INSTANCE.SetWindowPos(windowHandle, predecessor, 0, 0, 0, 0, User32.SWP_NOACTIVATE | User32.SWP_NOMOVE | User32.SWP_NOSIZE);
}
public void show() {
User32.INSTANCE.ShowWindow(windowHandle, User32.SW_RESTORE);
}
public void close() {
User32.INSTANCE.PostMessage(windowHandle, User32.WM_CLOSE, null, null);
}
public void minimize() {
User32.INSTANCE.ShowWindow(windowHandle,User32.SW_MINIMIZE);
}
public void move(Rect bounds) {
User32.INSTANCE.SetWindowPos(windowHandle, null, bounds.getX(), bounds.getY(), bounds.getW(), bounds.getH(), User32.SWP_NOACTIVATE);
}
public Rect getBounds() {
var rect = new WinDef.RECT();
User32.INSTANCE.GetWindowRect(windowHandle, rect);
return new Rect(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);
}
public boolean setWindowAttribute(int attribute, boolean attributeValue) {

View File

@@ -139,10 +139,17 @@ public class AppPrefs {
final BooleanProperty requireDoubleClickForConnections =
mapLocal(new SimpleBooleanProperty(false), "requireDoubleClickForConnections", Boolean.class, false);
final BooleanProperty enableTerminalDocking =
mapLocal(new SimpleBooleanProperty(true), "enableTerminalDocking", Boolean.class, true);
public ObservableBooleanValue requireDoubleClickForConnections() {
return requireDoubleClickForConnections;
}
public ObservableBooleanValue enableTerminalDocking() {
return enableTerminalDocking;
}
@Getter
private final Property<InPlaceSecretValue> lockPassword = new SimpleObjectProperty<>();

View File

@@ -10,12 +10,12 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.StackComp;
import io.xpipe.app.fxcomps.impl.TextFieldComp;
import io.xpipe.app.terminal.ExternalTerminalType;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.OptionsBuilder;
import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.app.terminal.TerminalLauncher;
import io.xpipe.app.terminal.TerminalView;
import io.xpipe.app.util.*;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ListCell;
@@ -24,6 +24,7 @@ import javafx.scene.paint.Color;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
import java.util.UUID;
public class TerminalCategory extends AppPrefsCategory {
@@ -44,7 +45,7 @@ public class TerminalCategory extends AppPrefsCategory {
"Test",
ProcessControlProvider.get()
.createLocalProcessControl(true)
.command("echo Test"));
.command("echo Test"), UUID.randomUUID());
}
});
})))
@@ -61,7 +62,11 @@ public class TerminalCategory extends AppPrefsCategory {
.hide(prefs.terminalType.isNotEqualTo(ExternalTerminalType.CUSTOM)))
.addComp(terminalTest)
.nameAndDescription("clearTerminalOnInit")
.addToggle(prefs.clearTerminalOnInit))
.addToggle(prefs.clearTerminalOnInit)
.nameAndDescription("enableTerminalDocking")
.addToggle(prefs.enableTerminalDocking)
.hide(new SimpleBooleanProperty(!TerminalView.isSupported()))
)
.buildComp();
}

View File

@@ -10,7 +10,7 @@ import io.xpipe.app.terminal.ExternalTerminalType;
import io.xpipe.app.util.DesktopHelper;
import io.xpipe.app.util.FileOpener;
import io.xpipe.app.util.OptionsBuilder;
import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.app.terminal.TerminalLauncher;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.util.XPipeInstallation;

View File

@@ -3,7 +3,7 @@ package io.xpipe.app.terminal;
import io.xpipe.app.util.LocalShell;
import io.xpipe.core.process.CommandBuilder;
public interface AlacrittyTerminalType extends ExternalTerminalType {
public interface AlacrittyTerminalType extends ExternalTerminalType, DockableTerminalType {
ExternalTerminalType ALACRITTY_WINDOWS = new Windows();
ExternalTerminalType ALACRITTY_LINUX = new Linux();

View File

@@ -0,0 +1,38 @@
package io.xpipe.app.terminal;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.ShellDialects;
public class CmdTerminalType extends ExternalTerminalType.SimplePathType implements DockableTerminalType {
public CmdTerminalType() {super("app.cmd", "cmd.exe", true);}
@Override
public int getProcessHierarchyOffset() {
return -1;
}
@Override
public boolean supportsTabs() {
return false;
}
@Override
public boolean isRecommended() {
return false;
}
@Override
public boolean supportsColoredTitle() {
return false;
}
@Override
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
if (configuration.getScriptDialect().equals(ShellDialects.CMD)) {
return CommandBuilder.of().add("/c").addFile(configuration.getScriptFile());
}
return CommandBuilder.of().add("/c").add(configuration.getDialectLaunchCommand());
}
}

View File

@@ -0,0 +1,8 @@
package io.xpipe.app.terminal;
public interface DockableTerminalType {
public default int getProcessHierarchyOffset() {
return 0;
}
}

View File

@@ -23,7 +23,6 @@ import lombok.Value;
import lombok.With;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
@@ -398,109 +397,12 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
}
};
ExternalTerminalType CMD = new SimplePathType("app.cmd", "cmd.exe", true) {
ExternalTerminalType CMD = new CmdTerminalType();
@Override
public boolean supportsTabs() {
return false;
}
ExternalTerminalType POWERSHELL = new PowerShellTerminalType();
@Override
public boolean isRecommended() {
return false;
}
ExternalTerminalType PWSH = new PwshTerminalType();
@Override
public boolean supportsColoredTitle() {
return false;
}
@Override
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
if (configuration.getScriptDialect().equals(ShellDialects.CMD)) {
return CommandBuilder.of().add("/c").addFile(configuration.getScriptFile());
}
return CommandBuilder.of().add("/c").add(configuration.getDialectLaunchCommand());
}
};
ExternalTerminalType POWERSHELL = new SimplePathType("app.powershell", "powershell", true) {
@Override
public boolean supportsTabs() {
return false;
}
@Override
public boolean isRecommended() {
return false;
}
@Override
public boolean supportsColoredTitle() {
return false;
}
@Override
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
if (configuration.getScriptDialect().equals(ShellDialects.POWERSHELL)) {
return CommandBuilder.of()
.add("-ExecutionPolicy", "Bypass")
.add("-File")
.addFile(configuration.getScriptFile());
}
return CommandBuilder.of()
.add("-ExecutionPolicy", "Bypass")
.add("-EncodedCommand")
.add(sc -> {
var base64 = Base64.getEncoder()
.encodeToString(configuration
.getDialectLaunchCommand()
.buildBase(sc)
.getBytes(StandardCharsets.UTF_16LE));
return "\"" + base64 + "\"";
});
}
};
ExternalTerminalType PWSH = new SimplePathType("app.pwsh", "pwsh", true) {
@Override
public String getWebsite() {
return "https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell?view=powershell-7.4";
}
@Override
public boolean supportsTabs() {
return false;
}
@Override
public boolean isRecommended() {
return false;
}
@Override
public boolean supportsColoredTitle() {
return false;
}
@Override
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
return CommandBuilder.of()
.add("-ExecutionPolicy", "Bypass")
.add("-EncodedCommand")
.add(sc -> {
// Fix for https://github.com/PowerShell/PowerShell/issues/18530#issuecomment-1325691850
var c = "$env:PSModulePath=\"\";"
+ configuration.getDialectLaunchCommand().buildBase(sc);
var base64 = Base64.getEncoder().encodeToString(c.getBytes(StandardCharsets.UTF_16LE));
return "\"" + base64 + "\"";
});
}
};
ExternalTerminalType GNOME_TERMINAL = new PathCheckType("app.gnomeTerminal", "gnome-terminal", true) {
@Override
public String getWebsite() {
@@ -1214,4 +1116,5 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
protected abstract CommandBuilder toCommand(LaunchConfiguration configuration) throws Exception;
}
}

View File

@@ -0,0 +1,40 @@
package io.xpipe.app.terminal;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.ShellDialects;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class PowerShellTerminalType extends ExternalTerminalType.SimplePathType implements DockableTerminalType {
public PowerShellTerminalType() {super("app.powershell", "powershell", true);}
@Override
public boolean supportsTabs() {
return false;
}
@Override
public boolean isRecommended() {
return false;
}
@Override
public boolean supportsColoredTitle() {
return false;
}
@Override
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
if (configuration.getScriptDialect().equals(ShellDialects.POWERSHELL)) {
return CommandBuilder.of().add("-ExecutionPolicy", "Bypass").add("-File").addFile(configuration.getScriptFile());
}
return CommandBuilder.of().add("-ExecutionPolicy", "Bypass").add("-EncodedCommand").add(sc -> {
var base64 = Base64.getEncoder().encodeToString(
configuration.getDialectLaunchCommand().buildBase(sc).getBytes(StandardCharsets.UTF_16LE));
return "\"" + base64 + "\"";
});
}
}

View File

@@ -0,0 +1,41 @@
package io.xpipe.app.terminal;
import io.xpipe.core.process.CommandBuilder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class PwshTerminalType extends ExternalTerminalType.SimplePathType implements DockableTerminalType {
public PwshTerminalType() {super("app.pwsh", "pwsh", true);}
@Override
public String getWebsite() {
return "https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell?view=powershell-7.4";
}
@Override
public boolean supportsTabs() {
return false;
}
@Override
public boolean isRecommended() {
return false;
}
@Override
public boolean supportsColoredTitle() {
return false;
}
@Override
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
return CommandBuilder.of().add("-ExecutionPolicy", "Bypass").add("-EncodedCommand").add(sc -> {
// Fix for https://github.com/PowerShell/PowerShell/issues/18530#issuecomment-1325691850
var c = "$env:PSModulePath=\"\";" + configuration.getDialectLaunchCommand().buildBase(sc);
var base64 = Base64.getEncoder().encodeToString(c.getBytes(StandardCharsets.UTF_16LE));
return "\"" + base64 + "\"";
});
}
}

View File

@@ -9,7 +9,7 @@ import io.xpipe.core.process.TerminalInitFunction;
import java.nio.file.Path;
import java.util.Optional;
public interface TabbyTerminalType extends ExternalTerminalType {
public interface TabbyTerminalType extends ExternalTerminalType, DockableTerminalType {
ExternalTerminalType TABBY_WINDOWS = new Windows();
ExternalTerminalType TABBY_MAC_OS = new MacOs();

View File

@@ -0,0 +1,75 @@
package io.xpipe.app.terminal;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppMainWindow;
import io.xpipe.app.fxcomps.SimpleComp;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.WindowEvent;
public class TerminalDockComp extends SimpleComp {
private final TerminalDockModel model;
public TerminalDockComp(TerminalDockModel model) {this.model = model;}
@Override
protected Region createSimple() {
var label = new Label();
label.textProperty().bind(AppI18n.observable("clickToDock"));
var stack = new StackPane(label);
stack.setAlignment(Pos.CENTER);
stack.setCursor(Cursor.HAND);
stack.boundsInParentProperty().addListener((observable, oldValue, newValue) -> {
update(stack);
});
var s = AppMainWindow.getInstance().getStage();
s.xProperty().addListener((observable, oldValue, newValue) -> {
update(stack);
});
s.yProperty().addListener((observable, oldValue, newValue) -> {
update(stack);
});
s.widthProperty().addListener((observable, oldValue, newValue) -> {
update(stack);
});
s.heightProperty().addListener((observable, oldValue, newValue) -> {
update(stack);
});
s.iconifiedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
model.onWindowMinimize();
} else {
model.onWindowActivate();
}
});
s.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
model.onFocusGain();
} else {
model.onFocusLost();
}
});
s.addEventFilter(WindowEvent.WINDOW_SHOWN,event -> {
update(stack);
});
s.addEventFilter(WindowEvent.WINDOW_HIDING,event -> {
model.onClose();
});
stack.setOnMouseClicked(event -> {
model.clickView();
event.consume();
});
return stack;
}
private void update(Region region) {
var bounds = region.localToScreen(region.getBoundsInLocal());
var sx = region.getScene().getWindow().getOutputScaleX();
var sy = region.getScene().getWindow().getOutputScaleY();
model.resizeView((int) Math.ceil(bounds.getMinX() * sx), (int) Math.ceil(bounds.getMinY() * sy),(int) Math.floor(bounds.getWidth() * sx), (int) Math.floor(bounds.getHeight() * sy));
}
}

View File

@@ -0,0 +1,172 @@
package io.xpipe.app.terminal;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.Rect;
import io.xpipe.core.process.OsType;
import java.util.ArrayList;
import java.util.List;
public class TerminalDockModel {
public static boolean isSupported() {
return OsType.getLocal() == OsType.WINDOWS;
}
private Rect viewBounds;
private boolean viewActive;
private final List<TerminalViewInstance> terminalInstances = new ArrayList<>();
public synchronized void trackTerminal(TerminalViewInstance terminal) {
terminalInstances.add(terminal);
}
public boolean isEnabled() {
return isSupported() && AppPrefs.get().enableTerminalDocking().get();
}
public synchronized void toggleView(boolean active) {
TrackEvent.withTrace("Terminal view toggled")
.tag("active", active)
.handle();
if (viewActive == active) {
return;
}
this.viewActive = active;
if (active) {
terminalInstances.forEach(terminalInstance -> terminalInstance.alwaysInFront());
updatePositions();
} else {
terminalInstances.forEach(terminalInstance -> terminalInstance.back());
}
}
public synchronized void onFocusGain() {
if (!viewActive) {
return;
}
TrackEvent.withTrace("Terminal view focus gained")
.handle();
terminalInstances.forEach(terminalInstance -> {
if (!terminalInstance.isActive()) {
return;
}
terminalInstance.updateBoundsState();
if (terminalInstance.isCustomBounds()) {
return;
}
terminalInstance.show();
terminalInstance.alwaysInFront();
});
}
public synchronized void onFocusLost() {
if (!viewActive) {
return;
}
TrackEvent.withTrace("Terminal view focus lost")
.handle();
terminalInstances.forEach(terminalInstance -> {
if (!terminalInstance.isActive()) {
return;
}
terminalInstance.updateBoundsState();
if (terminalInstance.isCustomBounds()) {
return;
}
terminalInstance.frontOfMainWindow();
});
}
public synchronized void onWindowActivate() {
TrackEvent.withTrace("Terminal view focus gained")
.handle();
terminalInstances.forEach(terminalInstance -> {
terminalInstance.updateBoundsState();
if (terminalInstance.isCustomBounds()) {
return;
}
terminalInstance.show();
if (viewActive) {
terminalInstance.alwaysInFront();
} else {
terminalInstance.back();
}
});
}
public synchronized void onWindowMinimize() {
TrackEvent.withTrace("Terminal view minimized")
.handle();
terminalInstances.forEach(terminalInstance -> {
terminalInstance.updateBoundsState();
if (terminalInstance.isCustomBounds()) {
return;
}
terminalInstance.minimize();
});
}
public synchronized void onClose() {
TrackEvent.withTrace("Terminal view closed")
.handle();
terminalInstances.forEach(terminalInstance -> {
terminalInstance.updateBoundsState();
if (terminalInstance.isCustomBounds()) {
return;
}
terminalInstance.close();
});
}
private void updatePositions() {
if (viewBounds == null) {
return;
}
terminalInstances.forEach(terminalInstance -> {
terminalInstance.updateBoundsState();
if (terminalInstance.isCustomBounds()) {
return;
}
terminalInstance.updatePosition(viewBounds);
});
}
public void resizeView(int x, int y, int w, int h) {
if (w < 100 || h < 100) {
return;
}
this.viewBounds = new Rect(x,y,w,h);
TrackEvent.withTrace("Terminal view resized")
.tag("rect", viewBounds)
.handle();
updatePositions();
}
public void clickView() {
TrackEvent.withTrace("Terminal view clicked")
.handle();
terminalInstances.forEach(terminalInstance -> {
terminalInstance.show();
terminalInstance.alwaysInFront();
terminalInstance.updatePosition(viewBounds);
});
}
}

View File

@@ -1,5 +1,7 @@
package io.xpipe.app.util;
package io.xpipe.app.terminal;
import io.xpipe.app.util.ScriptHelper;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.beacon.BeaconServerException;
import io.xpipe.core.process.*;
import io.xpipe.core.store.FilePath;

View File

@@ -1,4 +1,4 @@
package io.xpipe.app.util;
package io.xpipe.app.terminal;
import lombok.Value;

View File

@@ -1,4 +1,4 @@
package io.xpipe.app.util;
package io.xpipe.app.terminal;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppProperties;
@@ -7,7 +7,10 @@ import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.terminal.ExternalTerminalType;
import io.xpipe.app.util.LicenseProvider;
import io.xpipe.app.util.LicenseRequiredException;
import io.xpipe.app.util.LocalShell;
import io.xpipe.app.util.ScriptHelper;
import io.xpipe.core.process.*;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.util.FailableFunction;
@@ -22,15 +25,6 @@ import java.util.UUID;
public class TerminalLauncher {
public static void openDirect(String title, FailableFunction<ShellControl, String, Exception> command)
throws Exception {
var type = AppPrefs.get().terminalType().getValue();
if (type == null) {
throw ErrorEvent.expected(new IllegalStateException(AppI18n.get("noTerminalSet")));
}
openDirect(title, command, type);
}
public static void openDirect(
String title, FailableFunction<ShellControl, String, Exception> command, ExternalTerminalType type)
throws Exception {
@@ -53,10 +47,18 @@ public class TerminalLauncher {
}
public static void open(String title, ProcessControl cc) throws Exception {
open(null, title, null, cc);
open(null, title, null, cc, UUID.randomUUID());
}
public static void open(String title, ProcessControl cc, UUID request) throws Exception {
open(null, title, null, cc, request);
}
public static void open(DataStoreEntry entry, String title, String directory, ProcessControl cc) throws Exception {
open(entry, title, directory, cc, UUID.randomUUID());
}
public static void open(DataStoreEntry entry, String title, String directory, ProcessControl cc, UUID request) throws Exception {
var type = AppPrefs.get().terminalType().getValue();
if (type == null) {
throw ErrorEvent.expected(new IllegalStateException(AppI18n.get("noTerminalSet")));
@@ -73,7 +75,6 @@ public class TerminalLauncher {
&& type.shouldClear()
&& AppPrefs.get().clearTerminalOnInit().get(),
cc instanceof ShellControl ? type.additionalInitCommands() : TerminalInitFunction.none());
var request = UUID.randomUUID();
var config = createConfig(request, entry, cleanTitle, adjustedTitle);
var latch = TerminalLauncherManager.submitAsync(request, cc, terminalConfig, directory);
try {

View File

@@ -1,4 +1,4 @@
package io.xpipe.app.util;
package io.xpipe.app.terminal;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.beacon.BeaconClientException;

View File

@@ -0,0 +1,137 @@
package io.xpipe.app.terminal;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.window.NativeWinWindowControl;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.Rect;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.OsType;
import javafx.application.Platform;
import lombok.Value;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public class TerminalView {
public static boolean isSupported() {
return OsType.getLocal() == OsType.WINDOWS;
}
@Value
public static class Session {
UUID request;
ProcessHandle shell;
ProcessHandle terminal;
}
public static interface Listener {
void onSessionOpened(Session session);
void onSessionClosed(Session session);
void onTerminalOpened(TerminalViewInstance instance);
void onTerminalClosed(TerminalViewInstance instance);
}
private final List<Session> sessions = new ArrayList<>();
private final List<TerminalViewInstance> terminalInstances = new ArrayList<>();
private final List<Listener> listeners = new ArrayList<>();
public void addListener(Listener listener) {
this.listeners.add(listener);
}
public boolean isEnabled() {
return isSupported() && AppPrefs.get().enableTerminalDocking().get();
}
public synchronized void open(UUID request, long pid) {
var processHandle = ProcessHandle.of(pid);
if (processHandle.isEmpty() || !processHandle.get().isAlive()) {
return;
}
var shell = processHandle.get().parent();
if (shell.isEmpty()) {
return;
}
var terminal = getTerminalProcess(shell.get());
if (terminal.isEmpty()) {
return;
}
var session = new Session(request, shell.get(), terminal.get());
var instance = terminalInstances.stream().filter(i -> i.getTerminalProcess().equals(terminal.get())).findFirst();
if (instance.isEmpty()) {
var control = NativeWinWindowControl.byPid(terminal.get().pid());
if (control.isEmpty()) {
return;
}
var tv = new WindowsTerminalViewInstance(terminal.get(), control.get());
terminalInstances.add(tv);
listeners.forEach(listener -> listener.onTerminalOpened(tv));
}
sessions.add(session);
listeners.forEach(listener -> listener.onSessionOpened(session));
TrackEvent.withTrace("Terminal instance opened")
.tag("terminalPid", terminal.get().pid())
.tag("viewEnabled", isEnabled())
.handle();
if (!isEnabled()) {
return;
}
}
private Optional<ProcessHandle> getTerminalProcess(ProcessHandle shell) {
var t = AppPrefs.get().terminalType().getValue();
if (!(t instanceof DockableTerminalType dockableTerminalType)) {
return Optional.empty();
}
var off = dockableTerminalType.getProcessHierarchyOffset();
var current = Optional.of(shell);
for (int i = 0; i < 1 + off; i++) {
current = current.flatMap(processHandle -> processHandle.parent());
}
return current;
}
public synchronized void tick() {
sessions.removeIf(session -> !session.shell.isAlive() || !session.terminal.isAlive());
for (TerminalViewInstance terminalInstance : new ArrayList<>(terminalInstances)) {
var alive = terminalInstance.getTerminalProcess().isAlive();
if (!alive) {
terminalInstances.remove(terminalInstance);
TrackEvent.withTrace("Terminal session is dead").tag("pid", terminalInstance.getTerminalProcess().pid()).handle();
listeners.forEach(listener -> listener.onTerminalClosed(terminalInstance));
}
}
}
private static TerminalView INSTANCE;
public static void init() {
var instance = new TerminalView();
ThreadHelper.createPlatformThread("terminal-view", true, () -> {
while (true) {
instance.tick();
ThreadHelper.sleep(1000);
}
}).start();
INSTANCE = instance;
}
public static TerminalView get() {
return INSTANCE;
}
}

View File

@@ -0,0 +1,45 @@
package io.xpipe.app.terminal;
import io.xpipe.app.util.Rect;
import lombok.Getter;
@Getter
public abstract class TerminalViewInstance {
private final ProcessHandle terminalProcess;
protected Rect lastBounds;
protected boolean customBounds;
protected TerminalViewInstance(ProcessHandle terminalProcess) {this.terminalProcess = terminalProcess;}
public abstract void show();
public abstract void minimize();
public abstract void alwaysInFront();
public abstract void back();
public abstract void frontOfMainWindow();
public abstract void updatePosition(Rect bounds);
public abstract void close();
public abstract boolean isActive();
public abstract Rect queryBounds();
public final void updateBoundsState() {
if (!isActive()) {
return;
}
var bounds = queryBounds();
if (lastBounds != null && !lastBounds.equals(bounds)) {
customBounds = true;
}
lastBounds = bounds;
}
}

View File

@@ -11,7 +11,7 @@ import io.xpipe.core.store.FileNames;
import java.nio.file.Files;
import java.nio.file.Path;
public interface WindowsTerminalType extends ExternalTerminalType {
public interface WindowsTerminalType extends ExternalTerminalType, DockableTerminalType {
ExternalTerminalType WINDOWS_TERMINAL = new Standard();
ExternalTerminalType WINDOWS_TERMINAL_PREVIEW = new Preview();

View File

@@ -0,0 +1,70 @@
package io.xpipe.app.terminal;
import io.xpipe.app.core.window.NativeWinWindowControl;
import io.xpipe.app.util.Rect;
import lombok.AccessLevel;
import lombok.experimental.FieldDefaults;
@FieldDefaults(
makeFinal = true,
level = AccessLevel.PRIVATE)
public final class WindowsTerminalViewInstance extends TerminalViewInstance {
NativeWinWindowControl control;
public WindowsTerminalViewInstance(ProcessHandle terminal, NativeWinWindowControl control) {
super(terminal);
this.control = control;
}
@Override
public void show() {
this.control.show();
}
@Override
public void minimize() {
this.control.minimize();
}
@Override
public void alwaysInFront() {
this.control.alwaysInFront();
this.control.removeBorders();
}
@Override
public void back() {
control.defaultOrder();
NativeWinWindowControl.MAIN_WINDOW.alwaysInFront();
NativeWinWindowControl.MAIN_WINDOW.defaultOrder();
}
@Override
public void frontOfMainWindow() {
this.control.alwaysInFront();
this.control.defaultOrder();
}
@Override
public void updatePosition(Rect bounds) {
control.move(bounds);
this.lastBounds = bounds;
this.customBounds = false;
}
@Override
public void close() {
this.control.close();
}
@Override
public boolean isActive() {
return !control.isIconified();
}
@Override
public Rect queryBounds() {
return control.getBounds();
}
}

View File

@@ -7,7 +7,7 @@ import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.terminal.ExternalTerminalType;
import io.xpipe.app.util.LocalShell;
import io.xpipe.app.util.ScriptHelper;
import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.app.terminal.TerminalLauncher;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellDialects;

View File

@@ -0,0 +1,9 @@
package io.xpipe.app.util;
import lombok.Value;
@Value
public class Rect {
int x, y;
int w, h;
}

View File

@@ -7,7 +7,7 @@ import io.xpipe.app.issue.EventHandlerImpl;
import io.xpipe.app.storage.DataStateProviderImpl;
import io.xpipe.app.util.AppJacksonModule;
import io.xpipe.app.util.LicenseProvider;
import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.app.terminal.TerminalLauncher;
import io.xpipe.beacon.BeaconInterface;
import io.xpipe.core.util.DataStateProvider;
import io.xpipe.core.util.ModuleLayerLoader;

View File

@@ -22,6 +22,8 @@ public class TerminalWaitExchange extends BeaconInterface<TerminalWaitExchange.R
public static class Request {
@NonNull
UUID request;
long pid;
}
@Jacksonized

View File

@@ -7,7 +7,7 @@ import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.app.terminal.TerminalLauncher;
import io.xpipe.core.process.ShellStoreState;
import io.xpipe.core.process.ShellTtyState;
import io.xpipe.ext.base.script.ScriptHierarchy;
@@ -18,6 +18,7 @@ import javafx.beans.value.ObservableValue;
import lombok.Value;
import java.util.List;
import java.util.UUID;
public class RunScriptActionMenu implements ActionProvider {

View File

@@ -6,13 +6,14 @@ import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.app.terminal.TerminalLauncher;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.ShellControl;
import javafx.beans.value.ObservableValue;
import java.util.List;
import java.util.UUID;
public abstract class MultiExecuteAction implements BranchAction {
@@ -33,6 +34,8 @@ public abstract class MultiExecuteAction implements BranchAction {
continue;
}
var uuid = UUID.randomUUID();
model.getTerminalRequests().add(uuid);
TerminalLauncher.open(
model.getEntry().getEntry(),
entry.getRawFileEntry().getName(),
@@ -40,7 +43,8 @@ public abstract class MultiExecuteAction implements BranchAction {
? model.getCurrentDirectory()
.getPath()
: null,
cmd);
cmd,
uuid);
}
},
false);

View File

@@ -7,7 +7,7 @@ import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.app.terminal.TerminalLauncher;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.ProcessOutputException;
import io.xpipe.core.process.ShellControl;
@@ -15,6 +15,7 @@ import io.xpipe.core.process.ShellControl;
import javafx.beans.value.ObservableValue;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
public abstract class MultiExecuteSelectionAction implements BranchAction {
@@ -33,6 +34,8 @@ public abstract class MultiExecuteSelectionAction implements BranchAction {
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) {
model.withShell(
pc -> {
var uuid = UUID.randomUUID();
model.getTerminalRequests().add(uuid);
var cmd = pc.command(createCommand(pc, model, entries));
TerminalLauncher.open(
model.getEntry().getEntry(),
@@ -41,7 +44,8 @@ public abstract class MultiExecuteSelectionAction implements BranchAction {
? model.getCurrentDirectory()
.getPath()
: null,
cmd);
cmd,
uuid);
},
false);
}

View File

@@ -3,6 +3,7 @@ package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.action.LeafAction;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.store.FileKind;
@@ -26,6 +27,9 @@ public class OpenTerminalAction implements LeafAction {
model.getCurrentDirectory() != null
? model.getCurrentDirectory().getPath()
: null);
if (model.getBrowserModel() instanceof BrowserSessionModel sessionModel) {
sessionModel.
}
return;
}

View File

@@ -15,13 +15,15 @@ import io.xpipe.app.resources.SystemIcons;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.ShellStoreFormat;
import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.app.terminal.TerminalLauncher;
import io.xpipe.core.process.ShellStoreState;
import io.xpipe.ext.base.script.ScriptStore;
import javafx.beans.property.BooleanProperty;
import javafx.beans.value.ObservableValue;
import java.util.UUID;
public interface ShellStoreProvider extends DataStoreProvider {
@Override

View File

@@ -548,3 +548,4 @@ scriptsIntroStart=Get started
checkForSecurityUpdates=Check for security updates
#force
checkForSecurityUpdatesDescription=XPipe can check for potential security updates separately from normal feature updates. When this is enabled, at least important security updates will be recommended for installation even if the normal update check is disabled.\n\nDisabling this setting will result in no external version request being performed, and you won't be notified about any security updates.
clickToDock=Click to dock terminal