mirror of
https://github.com/xpipe-io/xpipe.git
synced 2026-04-23 08:00:56 -04:00
Dock rework
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<>();
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
38
app/src/main/java/io/xpipe/app/terminal/CmdTerminalType.java
Normal file
38
app/src/main/java/io/xpipe/app/terminal/CmdTerminalType.java
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.xpipe.app.terminal;
|
||||
|
||||
public interface DockableTerminalType {
|
||||
|
||||
public default int getProcessHierarchyOffset() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 + "\"";
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 + "\"";
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
172
app/src/main/java/io/xpipe/app/terminal/TerminalDockModel.java
Normal file
172
app/src/main/java/io/xpipe/app/terminal/TerminalDockModel.java
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.xpipe.app.util;
|
||||
package io.xpipe.app.terminal;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
@@ -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 {
|
||||
@@ -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;
|
||||
137
app/src/main/java/io/xpipe/app/terminal/TerminalView.java
Normal file
137
app/src/main/java/io/xpipe/app/terminal/TerminalView.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
9
app/src/main/java/io/xpipe/app/util/Rect.java
Normal file
9
app/src/main/java/io/xpipe/app/util/Rect.java
Normal file
@@ -0,0 +1,9 @@
|
||||
package io.xpipe.app.util;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
@Value
|
||||
public class Rect {
|
||||
int x, y;
|
||||
int w, h;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -22,6 +22,8 @@ public class TerminalWaitExchange extends BeaconInterface<TerminalWaitExchange.R
|
||||
public static class Request {
|
||||
@NonNull
|
||||
UUID request;
|
||||
|
||||
long pid;
|
||||
}
|
||||
|
||||
@Jacksonized
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user