mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-21 23:20:23 +00:00
Terminal rework
This commit is contained in:
parent
7fd07242f5
commit
b350222769
29 changed files with 825 additions and 764 deletions
|
@ -1,6 +1,7 @@
|
|||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.browser.BrowserAbstractSessionModel;
|
||||
import io.xpipe.app.browser.BrowserFullSessionModel;
|
||||
import io.xpipe.app.browser.BrowserStoreSessionTab;
|
||||
import io.xpipe.app.browser.action.BrowserAction;
|
||||
import io.xpipe.app.comp.Comp;
|
||||
|
@ -8,15 +9,13 @@ import io.xpipe.app.comp.base.ModalOverlayComp;
|
|||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.terminal.TerminalLauncher;
|
||||
import io.xpipe.app.terminal.*;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.process.CommandBuilder;
|
||||
import io.xpipe.core.process.ShellControl;
|
||||
import io.xpipe.core.process.ShellDialects;
|
||||
import io.xpipe.core.process.ShellOpenFunction;
|
||||
import io.xpipe.core.process.*;
|
||||
import io.xpipe.core.store.*;
|
||||
import io.xpipe.core.util.FailableConsumer;
|
||||
import io.xpipe.core.util.FailableRunnable;
|
||||
|
@ -206,6 +205,33 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
|||
cdSyncOrRetry(path, false).ifPresent(s -> cdSyncOrRetry(s, false));
|
||||
}
|
||||
|
||||
private boolean shouldLaunchSplitTerminal() {
|
||||
if (!AppPrefs.get().enableTerminalDocking().get()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (OsType.getLocal() != OsType.WINDOWS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var term = AppPrefs.get().terminalType().getValue();
|
||||
if (term == null || term.getOpenFormat() == TerminalOpenFormat.TABBED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(browserModel instanceof BrowserFullSessionModel f)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the right side is already occupied
|
||||
var existingSplit = f.getEffectiveRightTab().getValue();
|
||||
if (existingSplit != null && !(existingSplit instanceof BrowserTerminalDockTabModel)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public Optional<String> cdSyncOrRetry(String path, boolean customInput) {
|
||||
if (Objects.equals(path, currentPath.get())) {
|
||||
return Optional.empty();
|
||||
|
@ -250,27 +276,15 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
|||
if (ShellDialects.getStartableDialects().stream().anyMatch(dialect -> adjustedPath
|
||||
.toLowerCase()
|
||||
.startsWith(dialect.getExecutableName().toLowerCase()))) {
|
||||
var uuid = UUID.randomUUID();
|
||||
terminalRequests.add(uuid);
|
||||
TerminalLauncher.open(
|
||||
entry.getEntry(),
|
||||
name,
|
||||
directory,
|
||||
fileSystem
|
||||
.getShell()
|
||||
.get()
|
||||
.singularSubShell(
|
||||
ShellOpenFunction.of(CommandBuilder.ofString(adjustedPath), false)),
|
||||
uuid);
|
||||
var cc = fileSystem
|
||||
.getShell()
|
||||
.get()
|
||||
.singularSubShell(
|
||||
ShellOpenFunction.of(CommandBuilder.ofString(adjustedPath), false));
|
||||
openTerminalAsync(name,directory,cc);
|
||||
} else {
|
||||
var uuid = UUID.randomUUID();
|
||||
terminalRequests.add(uuid);
|
||||
TerminalLauncher.open(
|
||||
entry.getEntry(),
|
||||
name,
|
||||
directory,
|
||||
fileSystem.getShell().get().command(adjustedPath),
|
||||
uuid);
|
||||
var cc = fileSystem.getShell().get().command(adjustedPath);
|
||||
openTerminalAsync(name,directory,cc);
|
||||
}
|
||||
});
|
||||
return Optional.ofNullable(currentPath.get());
|
||||
|
@ -525,7 +539,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
|||
history.updateCurrent(null);
|
||||
}
|
||||
|
||||
public void openTerminalAsync(String directory) {
|
||||
public void openTerminalAsync(String name, String directory, ProcessControl processControl) {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
if (fileSystem == null) {
|
||||
return;
|
||||
|
@ -533,12 +547,14 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
|||
|
||||
BooleanScope.executeExclusive(busy, () -> {
|
||||
if (fileSystem.getShell().isPresent()) {
|
||||
var connection = fileSystem.getShell().get();
|
||||
var name = (directory != null ? directory + " - " : "")
|
||||
+ entry.get().getName();
|
||||
var dock = shouldLaunchSplitTerminal();
|
||||
var uuid = UUID.randomUUID();
|
||||
terminalRequests.add(uuid);
|
||||
TerminalLauncher.open(entry.getEntry(), name, directory, connection, uuid);
|
||||
if (dock && browserModel instanceof BrowserFullSessionModel fullSessionModel &&
|
||||
!(fullSessionModel.getSplits().get(this) instanceof BrowserTerminalDockTabModel)) {
|
||||
fullSessionModel.splitTab(this, new BrowserTerminalDockTabModel(browserModel, this, terminalRequests));
|
||||
}
|
||||
TerminalLauncher.open(entry.getEntry(), name, directory, processControl, uuid, !dock);
|
||||
|
||||
// Restart connection as we will have to start it anyway, so we speed it up by doing it preemptively
|
||||
startIfNeeded();
|
||||
|
|
|
@ -49,8 +49,6 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
|
|||
|
||||
@Override
|
||||
public void init() throws Exception {
|
||||
var sessions = new ArrayList<TerminalView.ShellSession>();
|
||||
var terminals = new ArrayList<TerminalView.TerminalSession>();
|
||||
listener = new TerminalView.Listener() {
|
||||
@Override
|
||||
public void onSessionOpened(TerminalView.ShellSession session) {
|
||||
|
@ -58,17 +56,11 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
|
|||
return;
|
||||
}
|
||||
|
||||
sessions.add(session);
|
||||
var tv = terminals.stream()
|
||||
.filter(instance -> sessions.stream()
|
||||
.anyMatch(s -> instance.getTerminalProcess().equals(s.getTerminal())))
|
||||
.map(terminalSession -> terminalSession.controllable())
|
||||
var sessions = TerminalView.get().getSessions();
|
||||
var tv = sessions.stream().filter(s -> terminalRequests.contains(s.getRequest()) && s.getTerminal().isRunning())
|
||||
.map(s -> s.getTerminal().controllable())
|
||||
.flatMap(Optional::stream)
|
||||
.toList();
|
||||
if (tv.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < tv.size() - 1; i++) {
|
||||
dockModel.closeTerminal(tv.get(i));
|
||||
}
|
||||
|
@ -77,20 +69,11 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
|
|||
dockModel.trackTerminal(toTrack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionClosed(TerminalView.ShellSession session) {
|
||||
sessions.remove(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminalOpened(TerminalView.TerminalSession instance) {
|
||||
terminals.add(instance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminalClosed(TerminalView.TerminalSession instance) {
|
||||
terminals.remove(instance);
|
||||
if (terminals.isEmpty()) {
|
||||
var sessions = TerminalView.get().getSessions();
|
||||
var remaining = sessions.stream().filter(s -> terminalRequests.contains(s.getRequest()) && s.getTerminal().isRunning()).toList();
|
||||
if (remaining.isEmpty()) {
|
||||
((BrowserFullSessionModel) browserModel).unsplitTab(BrowserTerminalDockTabModel.this);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,18 +12,22 @@ import com.sun.jna.platform.win32.User32;
|
|||
import com.sun.jna.platform.win32.WinDef;
|
||||
import com.sun.jna.platform.win32.WinNT;
|
||||
import com.sun.jna.ptr.IntByReference;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
public class NativeWinWindowControl {
|
||||
|
||||
public static Optional<NativeWinWindowControl> byPid(long pid) {
|
||||
var ref = new AtomicReference<NativeWinWindowControl>();
|
||||
public static List<NativeWinWindowControl> byPid(long pid) {
|
||||
var refs = new ArrayList<NativeWinWindowControl>();
|
||||
User32.INSTANCE.EnumWindows(
|
||||
(hWnd, data) -> {
|
||||
var visible = User32.INSTANCE.IsWindowVisible(hWnd);
|
||||
|
@ -34,14 +38,12 @@ public class NativeWinWindowControl {
|
|||
var wpid = new IntByReference();
|
||||
User32.INSTANCE.GetWindowThreadProcessId(hWnd, wpid);
|
||||
if (wpid.getValue() == pid) {
|
||||
ref.set(new NativeWinWindowControl(hWnd));
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
refs.add(new NativeWinWindowControl(hWnd));
|
||||
}
|
||||
return true;
|
||||
},
|
||||
null);
|
||||
return Optional.ofNullable(ref.get());
|
||||
return refs;
|
||||
}
|
||||
|
||||
public static NativeWinWindowControl MAIN_WINDOW;
|
||||
|
|
|
@ -10,13 +10,13 @@ public interface AlacrittyTerminalType extends ExternalTerminalType, TrackableTe
|
|||
ExternalTerminalType ALACRITTY_MAC_OS = new MacOs();
|
||||
|
||||
@Override
|
||||
default boolean supportsTabs() {
|
||||
return false;
|
||||
default String getWebsite() {
|
||||
return "https://github.com/alacritty/alacritty";
|
||||
}
|
||||
|
||||
@Override
|
||||
default String getWebsite() {
|
||||
return "https://github.com/alacritty/alacritty";
|
||||
default TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.NEW_WINDOW;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -36,7 +36,7 @@ public interface AlacrittyTerminalType extends ExternalTerminalType, TrackableTe
|
|||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
|
||||
protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {
|
||||
var b = CommandBuilder.of();
|
||||
|
||||
// if (configuration.getColor() != null) {
|
||||
|
@ -61,7 +61,7 @@ public interface AlacrittyTerminalType extends ExternalTerminalType, TrackableTe
|
|||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
|
||||
protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {
|
||||
return CommandBuilder.of()
|
||||
.add("-t")
|
||||
.addQuoted(configuration.getCleanTitle())
|
||||
|
@ -77,7 +77,7 @@ public interface AlacrittyTerminalType extends ExternalTerminalType, TrackableTe
|
|||
}
|
||||
|
||||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
LocalShell.getShell()
|
||||
.executeSimpleCommand(CommandBuilder.of()
|
||||
.add("open", "-a")
|
||||
|
|
|
@ -11,6 +11,11 @@ public class CmdTerminalType extends ExternalTerminalType.SimplePathType impleme
|
|||
super("app.cmd", "cmd.exe", true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.NEW_WINDOW;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProcessHierarchyOffset() {
|
||||
var powershell = ShellDialects.isPowershell(ProcessControlProvider.get().getEffectiveLocalDialect())
|
||||
|
@ -18,11 +23,6 @@ public class CmdTerminalType extends ExternalTerminalType.SimplePathType impleme
|
|||
return powershell ? 0 : -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecommended() {
|
||||
return false;
|
||||
|
@ -34,7 +34,7 @@ public class CmdTerminalType extends ExternalTerminalType.SimplePathType impleme
|
|||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
|
||||
protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {
|
||||
if (configuration.getScriptDialect().equals(ShellDialects.CMD)) {
|
||||
return CommandBuilder.of().add("/c").addFile(configuration.getScriptFile());
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@ public class CustomTerminalType extends ExternalApplicationType implements Exter
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return true;
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.NEW_WINDOW;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -31,7 +31,7 @@ public class CustomTerminalType extends ExternalApplicationType implements Exter
|
|||
}
|
||||
|
||||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
var custom = AppPrefs.get().customTerminalCommand().getValue();
|
||||
if (custom == null || custom.isBlank()) {
|
||||
throw ErrorEvent.expected(new IllegalStateException("No custom terminal command specified"));
|
||||
|
|
|
@ -1,29 +1,15 @@
|
|||
package io.xpipe.app.terminal;
|
||||
|
||||
import io.xpipe.app.comp.base.MarkdownComp;
|
||||
import io.xpipe.app.core.AppCache;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.window.AppWindowHelper;
|
||||
import io.xpipe.app.ext.PrefsChoiceValue;
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.prefs.ExternalApplicationType;
|
||||
import io.xpipe.app.storage.DataColor;
|
||||
import io.xpipe.app.util.*;
|
||||
import io.xpipe.core.process.*;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
import io.xpipe.core.util.FailableFunction;
|
||||
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonBar;
|
||||
import javafx.scene.control.ButtonType;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Value;
|
||||
import lombok.With;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
|
@ -97,305 +83,13 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
}
|
||||
|
||||
ExternalTerminalType XSHELL = new WindowsType("app.xShell", "Xshell") {
|
||||
ExternalTerminalType XSHELL = new XShellTerminalType();
|
||||
|
||||
@Override
|
||||
protected Optional<Path> determineInstallation() {
|
||||
try {
|
||||
var r = WindowsRegistry.local()
|
||||
.readValue(
|
||||
WindowsRegistry.HKEY_LOCAL_MACHINE,
|
||||
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\Xshell.exe");
|
||||
return r.map(Path::of);
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).omit().handle();
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
ExternalTerminalType SECURECRT = new SecureCrtTerminalType();
|
||||
|
||||
@Override
|
||||
public String getWebsite() {
|
||||
return "https://www.netsarang.com/en/xshell/";
|
||||
}
|
||||
ExternalTerminalType MOBAXTERM = new MobaXTermTerminalType();
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecommended() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsColoredTitle() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(Path file, LaunchConfiguration configuration) throws Exception {
|
||||
SshLocalBridge.init();
|
||||
if (!showInfo()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (var sc = LocalShell.getShell()) {
|
||||
var b = SshLocalBridge.get();
|
||||
var keyName = b.getIdentityKey().getFileName().toString();
|
||||
var command = CommandBuilder.of()
|
||||
.addFile(file.toString())
|
||||
.add("-url")
|
||||
.addQuoted("ssh://" + b.getUser() + "@localhost:" + b.getPort())
|
||||
.add("-i", keyName);
|
||||
sc.executeSimpleCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean showInfo() {
|
||||
boolean set = AppCache.getBoolean("xshellSetup", false);
|
||||
if (set) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var b = SshLocalBridge.get();
|
||||
var keyName = b.getIdentityKey().getFileName().toString();
|
||||
var r = AppWindowHelper.showBlockingAlert(alert -> {
|
||||
alert.setTitle(AppI18n.get("xshellSetup"));
|
||||
alert.setAlertType(Alert.AlertType.NONE);
|
||||
|
||||
var activated = AppI18n.get()
|
||||
.getMarkdownDocumentation("app:xshellSetup")
|
||||
.formatted(b.getIdentityKey(), keyName);
|
||||
var markdown = new MarkdownComp(activated, s -> s)
|
||||
.prefWidth(450)
|
||||
.prefHeight(400)
|
||||
.createRegion();
|
||||
alert.getDialogPane().setContent(markdown);
|
||||
|
||||
alert.getButtonTypes().add(new ButtonType(AppI18n.get("ok"), ButtonBar.ButtonData.OK_DONE));
|
||||
});
|
||||
r.filter(buttonType -> buttonType.getButtonData().isDefaultButton());
|
||||
r.ifPresent(buttonType -> {
|
||||
AppCache.update("xshellSetup", true);
|
||||
});
|
||||
return r.isPresent();
|
||||
}
|
||||
};
|
||||
|
||||
ExternalTerminalType SECURECRT = new WindowsType("app.secureCrt", "SecureCRT") {
|
||||
|
||||
@Override
|
||||
protected Optional<Path> determineInstallation() {
|
||||
try (var sc = LocalShell.getShell().start()) {
|
||||
var env = sc.executeSimpleStringCommand(
|
||||
sc.getShellDialect().getPrintEnvironmentVariableCommand("ProgramFiles"));
|
||||
var file = Path.of(env, "VanDyke Software\\SecureCRT\\SecureCRT.exe");
|
||||
if (!Files.exists(file)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(file);
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).omit().handle();
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecommended() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsColoredTitle() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWebsite() {
|
||||
return "https://www.vandyke.com/products/securecrt/";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(Path file, LaunchConfiguration configuration) throws Exception {
|
||||
try (var sc = LocalShell.getShell()) {
|
||||
SshLocalBridge.init();
|
||||
var b = SshLocalBridge.get();
|
||||
var command = CommandBuilder.of()
|
||||
.addFile(file.toString())
|
||||
.add("/T")
|
||||
.add("/SSH2", "/ACCEPTHOSTKEYS", "/I")
|
||||
.addFile(b.getIdentityKey().toString())
|
||||
.add("/P", "" + b.getPort())
|
||||
.add("/L")
|
||||
.addQuoted(b.getUser())
|
||||
.add("localhost");
|
||||
sc.executeSimpleCommand(command);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ExternalTerminalType MOBAXTERM = new WindowsType("app.mobaXterm", "MobaXterm") {
|
||||
|
||||
@Override
|
||||
protected Optional<Path> determineInstallation() {
|
||||
try {
|
||||
var r = WindowsRegistry.local()
|
||||
.readValue(WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\Classes\\mobaxterm\\DefaultIcon");
|
||||
return r.map(Path::of);
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).omit().handle();
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecommended() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsColoredTitle() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWebsite() {
|
||||
return "https://mobaxterm.mobatek.net/";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(Path file, LaunchConfiguration configuration) throws Exception {
|
||||
try (var sc = LocalShell.getShell()) {
|
||||
SshLocalBridge.init();
|
||||
var b = SshLocalBridge.get();
|
||||
var command = CommandBuilder.of()
|
||||
.addFile("ssh")
|
||||
.addQuoted(b.getUser() + "@localhost")
|
||||
.add("-i")
|
||||
.add("\"$(cygpath \"" + b.getIdentityKey().toString() + "\")\"")
|
||||
.add("-p")
|
||||
.add("" + b.getPort());
|
||||
// Don't use local shell to build as it uses cygwin
|
||||
var rawCommand = command.buildSimple();
|
||||
var script = ScriptHelper.getExecScriptFile(sc, "sh");
|
||||
Files.writeString(Path.of(script.toString()), rawCommand);
|
||||
var fixedFile = script.toString().replaceAll("\\\\", "/").replaceAll("\\s", "\\$0");
|
||||
sc.command(CommandBuilder.of()
|
||||
.addFile(file.toString())
|
||||
.add("-newtab")
|
||||
.add(fixedFile))
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ExternalTerminalType TERMIUS = new ExternalTerminalType() {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "app.termius";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
try (var sc = LocalShell.getShell()) {
|
||||
return switch (OsType.getLocal()) {
|
||||
case OsType.Linux linux -> {
|
||||
yield Files.exists(Path.of("/opt/Termius"));
|
||||
}
|
||||
case OsType.MacOs macOs -> {
|
||||
yield Files.exists(Path.of("/Applications/Termius.app"));
|
||||
}
|
||||
case OsType.Windows windows -> {
|
||||
var r = WindowsRegistry.local()
|
||||
.readValue(WindowsRegistry.HKEY_CURRENT_USER, "SOFTWARE\\Classes\\termius");
|
||||
yield r.isPresent();
|
||||
}
|
||||
};
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).omit().handle();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWebsite() {
|
||||
return "https://termius.com/";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecommended() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsColoredTitle() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
SshLocalBridge.init();
|
||||
if (!showInfo()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var host = "localhost";
|
||||
var b = SshLocalBridge.get();
|
||||
var port = b.getPort();
|
||||
var user = b.getUser();
|
||||
var name = b.getIdentityKey().getFileName().toString();
|
||||
Hyperlinks.open("termius://app/host-sharing#label=" + name + "&ip=" + host + "&port=" + port + "&username="
|
||||
+ user + "&os=undefined");
|
||||
}
|
||||
|
||||
private boolean showInfo() throws IOException {
|
||||
boolean set = AppCache.getBoolean("termiusSetup", false);
|
||||
if (set) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var b = SshLocalBridge.get();
|
||||
var keyContent = Files.readString(b.getIdentityKey());
|
||||
var r = AppWindowHelper.showBlockingAlert(alert -> {
|
||||
alert.setTitle(AppI18n.get("termiusSetup"));
|
||||
alert.setAlertType(Alert.AlertType.NONE);
|
||||
|
||||
var activated = AppI18n.get()
|
||||
.getMarkdownDocumentation("app:termiusSetup")
|
||||
.formatted(b.getIdentityKey(), keyContent);
|
||||
var markdown = new MarkdownComp(activated, s -> s)
|
||||
.prefWidth(450)
|
||||
.prefHeight(450)
|
||||
.createRegion();
|
||||
alert.getDialogPane().setContent(markdown);
|
||||
|
||||
alert.getButtonTypes().add(new ButtonType(AppI18n.get("ok"), ButtonBar.ButtonData.OK_DONE));
|
||||
});
|
||||
r.filter(buttonType -> buttonType.getButtonData().isDefaultButton());
|
||||
r.ifPresent(buttonType -> {
|
||||
AppCache.update("termiusSetup", true);
|
||||
});
|
||||
return r.isPresent();
|
||||
}
|
||||
};
|
||||
ExternalTerminalType TERMIUS = new TermiusTerminalType();
|
||||
|
||||
ExternalTerminalType CMD = new CmdTerminalType();
|
||||
|
||||
|
@ -413,8 +107,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return true;
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.NEW_WINDOW_OR_TABBED;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -428,11 +122,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
|
||||
protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {
|
||||
// Note for later: When debugging konsole launches, it will always open as a child process of
|
||||
// IntelliJ/XPipe even though we try to detach it.
|
||||
// This is not the case for production where it works as expected
|
||||
return CommandBuilder.of().add("--new-tab", "-e").addFile(configuration.getScriptFile());
|
||||
return CommandBuilder.of().addIf(configuration.isPreferTabs(), "--new-tab").add("-e").addFile(configuration.getScriptFile());
|
||||
}
|
||||
};
|
||||
ExternalTerminalType XFCE = new SimplePathType("app.xfce", "xfce4-terminal", true) {
|
||||
|
@ -442,8 +136,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return true;
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.NEW_WINDOW_OR_TABBED;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -457,9 +151,10 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
|
||||
protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {
|
||||
return CommandBuilder.of()
|
||||
.add("--tab", "--title")
|
||||
.addIf(configuration.isPreferTabs(),"--tab")
|
||||
.add("--title")
|
||||
.addQuoted(configuration.getColoredTitle())
|
||||
.add("--command")
|
||||
.addFile(configuration.getScriptFile());
|
||||
|
@ -472,8 +167,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return false;
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.NEW_WINDOW;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -487,7 +182,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
|
||||
protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {
|
||||
return CommandBuilder.of()
|
||||
.add("--title")
|
||||
.addQuoted(configuration.getColoredTitle())
|
||||
|
@ -502,8 +197,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return true;
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.NEW_WINDOW_OR_TABBED;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -517,8 +212,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
|
||||
return CommandBuilder.of().add("--new-tab").add("-e").addFile(configuration.getScriptFile());
|
||||
protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {
|
||||
return CommandBuilder.of().addIf(configuration.isPreferTabs(), "--new-tab").add("-e").addFile(configuration.getScriptFile());
|
||||
}
|
||||
};
|
||||
ExternalTerminalType TILIX = new SimplePathType("app.tilix", "tilix", true) {
|
||||
|
@ -528,8 +223,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return false;
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.NEW_WINDOW;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -543,7 +238,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
|
||||
protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {
|
||||
return CommandBuilder.of()
|
||||
.add("-t")
|
||||
.addQuoted(configuration.getColoredTitle())
|
||||
|
@ -557,11 +252,6 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
return "https://gnome-terminator.org/";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecommended() {
|
||||
return true;
|
||||
|
@ -573,13 +263,18 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.NEW_WINDOW_OR_TABBED;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {
|
||||
return CommandBuilder.of()
|
||||
.add("-e")
|
||||
.addFile(configuration.getScriptFile())
|
||||
.add("-T")
|
||||
.addQuoted(configuration.getColoredTitle())
|
||||
.add("--new-tab");
|
||||
.addIf(configuration.isPreferTabs(), "--new-tab");
|
||||
}
|
||||
};
|
||||
ExternalTerminalType TERMINOLOGY = new SimplePathType("app.terminology", "terminology", true) {
|
||||
|
@ -588,11 +283,6 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
return "https://github.com/borisfaure/terminology";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecommended() {
|
||||
return true;
|
||||
|
@ -604,8 +294,14 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.NEW_WINDOW_OR_TABBED;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {
|
||||
return CommandBuilder.of()
|
||||
.addIf(!configuration.isPreferTabs(), "-s")
|
||||
.add("-T")
|
||||
.addQuoted(configuration.getColoredTitle())
|
||||
.add("-2")
|
||||
|
@ -625,11 +321,6 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
return "https://github.com/Guake/guake";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecommended() {
|
||||
return true;
|
||||
|
@ -641,7 +332,12 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.TABBED;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {
|
||||
return CommandBuilder.of()
|
||||
.add("-n", "~")
|
||||
.add("-r")
|
||||
|
@ -656,11 +352,6 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
return "https://github.com/lanoxx/tilda";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecommended() {
|
||||
return true;
|
||||
|
@ -672,7 +363,12 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.TABBED;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {
|
||||
return CommandBuilder.of().add("-c").addFile(configuration.getScriptFile());
|
||||
}
|
||||
};
|
||||
|
@ -683,8 +379,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return false;
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.NEW_WINDOW;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -698,7 +394,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
|
||||
protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {
|
||||
return CommandBuilder.of()
|
||||
.add("-title")
|
||||
.addQuoted(configuration.getColoredTitle())
|
||||
|
@ -719,8 +415,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return false;
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.NEW_WINDOW;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -734,7 +430,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
|
||||
protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {
|
||||
return CommandBuilder.of().add("-C").addFile(configuration.getScriptFile());
|
||||
}
|
||||
};
|
||||
|
@ -751,8 +447,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return false;
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.NEW_WINDOW;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -766,22 +462,22 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
|
||||
protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {
|
||||
return CommandBuilder.of().add("-e").add(configuration.getDialectLaunchCommand());
|
||||
}
|
||||
};
|
||||
ExternalTerminalType MACOS_TERMINAL = new MacOsType("app.macosTerminal", "Terminal") {
|
||||
|
||||
@Override
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.TABBED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProcessHierarchyOffset() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecommended() {
|
||||
return false;
|
||||
|
@ -793,7 +489,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
LocalShell.getShell()
|
||||
.executeSimpleCommand(CommandBuilder.of()
|
||||
.add("open", "-a")
|
||||
|
@ -803,6 +499,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
};
|
||||
ExternalTerminalType ITERM2 = new MacOsType("app.iterm2", "iTerm") {
|
||||
|
||||
@Override
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.TABBED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProcessHierarchyOffset() {
|
||||
return 3;
|
||||
|
@ -813,11 +514,6 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
return "https://iterm2.com/";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecommended() {
|
||||
return true;
|
||||
|
@ -829,7 +525,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
LocalShell.getShell()
|
||||
.executeSimpleCommand(CommandBuilder.of()
|
||||
.add("open", "-a")
|
||||
|
@ -837,75 +533,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
.addFile(configuration.getScriptFile()));
|
||||
}
|
||||
};
|
||||
ExternalTerminalType WARP = new MacOsType("app.warp", "Warp") {
|
||||
|
||||
@Override
|
||||
public int getProcessHierarchyOffset() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWebsite() {
|
||||
return "https://www.warp.dev/";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecommended() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsColoredTitle() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldClear() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
LocalShell.getShell()
|
||||
.executeSimpleCommand(CommandBuilder.of()
|
||||
.add("open", "-a")
|
||||
.addQuoted("Warp.app")
|
||||
.addFile(configuration.getScriptFile()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public FailableFunction<LaunchConfiguration, String, Exception> remoteLaunchCommand(
|
||||
ShellDialect systemDialect) {
|
||||
return launchConfiguration -> {
|
||||
var toExecute = CommandBuilder.of()
|
||||
.add("open", "-a")
|
||||
.addQuoted("Warp.app")
|
||||
.addFile(launchConfiguration.getScriptFile());
|
||||
return toExecute.buildSimple();
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public TerminalInitFunction additionalInitCommands() {
|
||||
return TerminalInitFunction.of(sc -> {
|
||||
if (sc.getShellDialect() == ShellDialects.ZSH) {
|
||||
return "printf '\\eP$f{\"hook\": \"SourcedRcFileForWarp\", \"value\": { \"shell\": \"zsh\"}}\\x9c'";
|
||||
}
|
||||
if (sc.getShellDialect() == ShellDialects.BASH) {
|
||||
return "printf '\\eP$f{\"hook\": \"SourcedRcFileForWarp\", \"value\": { \"shell\": \"bash\"}}\\x9c'";
|
||||
}
|
||||
if (sc.getShellDialect() == ShellDialects.FISH) {
|
||||
return "printf '\\eP$f{\"hook\": \"SourcedRcFileForWarp\", \"value\": { \"shell\": \"fish\"}}\\x9c'";
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
};
|
||||
ExternalTerminalType WARP = new WarpTerminalType();
|
||||
ExternalTerminalType CUSTOM = new CustomTerminalType();
|
||||
List<ExternalTerminalType> WINDOWS_TERMINALS = List.of(
|
||||
WindowsTerminalType.WINDOWS_TERMINAL_CANARY,
|
||||
|
@ -998,7 +626,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
return TerminalInitFunction.none();
|
||||
}
|
||||
|
||||
boolean supportsTabs();
|
||||
TerminalOpenFormat getOpenFormat();
|
||||
|
||||
default String getWebsite() {
|
||||
return null;
|
||||
|
@ -1012,9 +640,9 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
return true;
|
||||
}
|
||||
|
||||
default void launch(LaunchConfiguration configuration) throws Exception {}
|
||||
default void launch(TerminalLaunchConfiguration configuration) throws Exception {}
|
||||
|
||||
default FailableFunction<LaunchConfiguration, String, Exception> remoteLaunchCommand(ShellDialect systemDialect) {
|
||||
default FailableFunction<TerminalLaunchConfiguration, String, Exception> remoteLaunchCommand(ShellDialect systemDialect) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -1025,7 +653,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
var location = determineFromPath();
|
||||
if (location.isEmpty()) {
|
||||
location = determineInstallation();
|
||||
|
@ -1038,24 +666,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
execute(location.get(), configuration);
|
||||
}
|
||||
|
||||
protected abstract void execute(Path file, LaunchConfiguration configuration) throws Exception;
|
||||
}
|
||||
|
||||
@Value
|
||||
class LaunchConfiguration {
|
||||
DataColor color;
|
||||
String coloredTitle;
|
||||
String cleanTitle;
|
||||
|
||||
@With
|
||||
FilePath scriptFile;
|
||||
|
||||
ShellDialect scriptDialect;
|
||||
|
||||
public CommandBuilder getDialectLaunchCommand() {
|
||||
var open = scriptDialect.getOpenScriptCommand(scriptFile.toString());
|
||||
return open;
|
||||
}
|
||||
protected abstract void execute(Path file, TerminalLaunchConfiguration configuration) throws Exception;
|
||||
}
|
||||
|
||||
abstract class MacOsType extends ExternalApplicationType.MacApplication
|
||||
|
@ -1082,13 +693,13 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
var args = toCommand(configuration);
|
||||
launch(configuration.getColoredTitle(), args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FailableFunction<LaunchConfiguration, String, Exception> remoteLaunchCommand(
|
||||
public FailableFunction<TerminalLaunchConfiguration, String, Exception> remoteLaunchCommand(
|
||||
ShellDialect systemDialect) {
|
||||
return launchConfiguration -> {
|
||||
var args = toCommand(launchConfiguration);
|
||||
|
@ -1100,6 +711,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
|
|||
};
|
||||
}
|
||||
|
||||
protected abstract CommandBuilder toCommand(LaunchConfiguration configuration) throws Exception;
|
||||
protected abstract CommandBuilder toCommand(TerminalLaunchConfiguration configuration) throws Exception;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -14,13 +14,13 @@ public class GnomeTerminalType extends ExternalTerminalType.PathCheckType implem
|
|||
}
|
||||
|
||||
@Override
|
||||
public String getWebsite() {
|
||||
return "https://help.gnome.org/users/gnome-terminal/stable/";
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.NEW_WINDOW;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return false;
|
||||
public String getWebsite() {
|
||||
return "https://help.gnome.org/users/gnome-terminal/stable/";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -34,7 +34,7 @@ public class GnomeTerminalType extends ExternalTerminalType.PathCheckType implem
|
|||
}
|
||||
|
||||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
try (ShellControl pc = LocalShell.getShell()) {
|
||||
CommandSupport.isInPathOrThrow(pc, executable, toTranslatedString().getValue(), null);
|
||||
|
||||
|
@ -51,7 +51,7 @@ public class GnomeTerminalType extends ExternalTerminalType.PathCheckType implem
|
|||
}
|
||||
|
||||
@Override
|
||||
public FailableFunction<LaunchConfiguration, String, Exception> remoteLaunchCommand(ShellDialect systemDialect) {
|
||||
public FailableFunction<TerminalLaunchConfiguration, String, Exception> remoteLaunchCommand(ShellDialect systemDialect) {
|
||||
return launchConfiguration -> {
|
||||
var toExecute = CommandBuilder.of()
|
||||
.add(executable, "-v", "--title")
|
||||
|
|
|
@ -27,7 +27,7 @@ public interface KittyTerminalType extends ExternalTerminalType, TrackableTermin
|
|||
}
|
||||
}
|
||||
|
||||
private static void open(ExternalTerminalType.LaunchConfiguration configuration, CommandBuilder socketWrite)
|
||||
private static void open(TerminalLaunchConfiguration configuration, CommandBuilder socketWrite)
|
||||
throws Exception {
|
||||
try (var sc = LocalShell.getShell().start()) {
|
||||
var payload = JsonNodeFactory.instance.objectNode();
|
||||
|
@ -73,11 +73,6 @@ public interface KittyTerminalType extends ExternalTerminalType, TrackableTermin
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
default boolean supportsTabs() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
default String getWebsite() {
|
||||
return "https://github.com/kovidgoyal/kitty";
|
||||
|
@ -89,6 +84,11 @@ public interface KittyTerminalType extends ExternalTerminalType, TrackableTermin
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
default TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.TABBED;
|
||||
}
|
||||
|
||||
@Override
|
||||
default boolean supportsColoredTitle() {
|
||||
return true;
|
||||
|
@ -116,7 +116,7 @@ public interface KittyTerminalType extends ExternalTerminalType, TrackableTermin
|
|||
}
|
||||
|
||||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
try (var sc = LocalShell.getShell().start()) {
|
||||
CommandSupport.isInPathOrThrow(sc, "kitty", "Kitty", null);
|
||||
CommandSupport.isInPathOrThrow(sc, "socat", "socat", null);
|
||||
|
@ -167,7 +167,7 @@ public interface KittyTerminalType extends ExternalTerminalType, TrackableTermin
|
|||
}
|
||||
|
||||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
// We use the absolute path to force the usage of macOS netcat
|
||||
// Homebrew versions have different option formats
|
||||
try (var sc = LocalShell.getShell().start()) {
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package io.xpipe.app.terminal;
|
||||
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.util.LocalShell;
|
||||
import io.xpipe.app.util.ScriptHelper;
|
||||
import io.xpipe.app.util.SshLocalBridge;
|
||||
import io.xpipe.app.util.WindowsRegistry;
|
||||
import io.xpipe.core.process.CommandBuilder;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
public class MobaXTermTerminalType extends ExternalTerminalType.WindowsType {
|
||||
|
||||
public MobaXTermTerminalType() {super("app.mobaXterm", "MobaXterm");}
|
||||
|
||||
@Override
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.TABBED;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<Path> determineInstallation() {
|
||||
try {
|
||||
var r = WindowsRegistry.local().readValue(WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\Classes\\mobaxterm\\DefaultIcon");
|
||||
return r.map(Path::of);
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).omit().handle();
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecommended() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsColoredTitle() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWebsite() {
|
||||
return "https://mobaxterm.mobatek.net/";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(Path file, TerminalLaunchConfiguration configuration) throws Exception {
|
||||
try (var sc = LocalShell.getShell()) {
|
||||
SshLocalBridge.init();
|
||||
var b = SshLocalBridge.get();
|
||||
var command = CommandBuilder.of().addFile("ssh").addQuoted(b.getUser() + "@localhost").add("-i").add(
|
||||
"\"$(cygpath \"" + b.getIdentityKey().toString() + "\")\"").add("-p").add("" + b.getPort());
|
||||
// Don't use local shell to build as it uses cygwin
|
||||
var rawCommand = command.buildSimple();
|
||||
var script = ScriptHelper.getExecScriptFile(sc, "sh");
|
||||
Files.writeString(Path.of(script.toString()), rawCommand);
|
||||
var fixedFile = script.toString().replaceAll("\\\\", "/").replaceAll("\\s", "\\$0");
|
||||
sc.command(CommandBuilder.of().addFile(file.toString()).add("-newtab").add(fixedFile)).execute();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,15 +15,15 @@ public class PowerShellTerminalType extends ExternalTerminalType.SimplePathType
|
|||
}
|
||||
|
||||
@Override
|
||||
public int getProcessHierarchyOffset() {
|
||||
var powershell = ProcessControlProvider.get().getEffectiveLocalDialect() == POWERSHELL
|
||||
|| AppPrefs.get().enableTerminalLogging().get();
|
||||
return powershell ? -1 : 0;
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.NEW_WINDOW;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return false;
|
||||
public int getProcessHierarchyOffset() {
|
||||
var powershell = ProcessControlProvider.get().getEffectiveLocalDialect() == ShellDialects.POWERSHELL
|
||||
|| AppPrefs.get().enableTerminalLogging().get();
|
||||
return powershell ? -1 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -37,7 +37,7 @@ public class PowerShellTerminalType extends ExternalTerminalType.SimplePathType
|
|||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
|
||||
protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {
|
||||
if (configuration.getScriptDialect().equals(ShellDialects.POWERSHELL)) {
|
||||
return CommandBuilder.of()
|
||||
.add("-ExecutionPolicy", "Bypass")
|
||||
|
|
|
@ -12,13 +12,13 @@ public class PwshTerminalType extends ExternalTerminalType.SimplePathType implem
|
|||
}
|
||||
|
||||
@Override
|
||||
public String getWebsite() {
|
||||
return "https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell?view=powershell-7.4";
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.NEW_WINDOW;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsTabs() {
|
||||
return false;
|
||||
public String getWebsite() {
|
||||
return "https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell?view=powershell-7.4";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -32,7 +32,7 @@ public class PwshTerminalType extends ExternalTerminalType.SimplePathType implem
|
|||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
|
||||
protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) {
|
||||
return CommandBuilder.of()
|
||||
.add("-ExecutionPolicy", "Bypass")
|
||||
.add("-EncodedCommand")
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
package io.xpipe.app.terminal;
|
||||
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.util.LocalShell;
|
||||
import io.xpipe.app.util.SshLocalBridge;
|
||||
import io.xpipe.core.process.CommandBuilder;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
public class SecureCrtTerminalType extends ExternalTerminalType.WindowsType {
|
||||
|
||||
public SecureCrtTerminalType() {super("app.secureCrt", "SecureCRT");}
|
||||
|
||||
@Override
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.TABBED;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<Path> determineInstallation() {
|
||||
try (var sc = LocalShell.getShell().start()) {
|
||||
var env = sc.executeSimpleStringCommand(sc.getShellDialect().getPrintEnvironmentVariableCommand("ProgramFiles"));
|
||||
var file = Path.of(env, "VanDyke Software\\SecureCRT\\SecureCRT.exe");
|
||||
if (!Files.exists(file)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(file);
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).omit().handle();
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecommended() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsColoredTitle() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWebsite() {
|
||||
return "https://www.vandyke.com/products/securecrt/";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(Path file, TerminalLaunchConfiguration configuration) throws Exception {
|
||||
try (var sc = LocalShell.getShell()) {
|
||||
SshLocalBridge.init();
|
||||
var b = SshLocalBridge.get();
|
||||
var command = CommandBuilder.of().addFile(file.toString()).add("/T").add("/SSH2", "/ACCEPTHOSTKEYS", "/I").addFile(
|
||||
b.getIdentityKey().toString()).add("/P", "" + b.getPort()).add("/L").addQuoted(b.getUser()).add("localhost");
|
||||
sc.executeSimpleCommand(command);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,8 +15,8 @@ public interface TabbyTerminalType extends ExternalTerminalType, TrackableTermin
|
|||
ExternalTerminalType TABBY_MAC_OS = new MacOs();
|
||||
|
||||
@Override
|
||||
default boolean supportsTabs() {
|
||||
return true;
|
||||
default TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.TABBED;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -67,7 +67,7 @@ public interface TabbyTerminalType extends ExternalTerminalType, TrackableTermin
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void execute(Path file, LaunchConfiguration configuration) throws Exception {
|
||||
protected void execute(Path file, TerminalLaunchConfiguration configuration) throws Exception {
|
||||
// Tabby has a very weird handling of output, even detaching with start does not prevent it from printing
|
||||
if (configuration.getScriptDialect().equals(ShellDialects.CMD)) {
|
||||
// It also freezes with any other input than .bat files, why?
|
||||
|
@ -124,7 +124,7 @@ public interface TabbyTerminalType extends ExternalTerminalType, TrackableTermin
|
|||
}
|
||||
|
||||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
LocalShell.getShell()
|
||||
.executeSimpleCommand(CommandBuilder.of()
|
||||
.add("open", "-a")
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
package io.xpipe.app.terminal;
|
||||
|
||||
import io.xpipe.app.core.AppProperties;
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.DataColor;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
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.CommandBuilder;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.process.ShellDialect;
|
||||
import io.xpipe.core.process.ShellDialects;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
import lombok.Value;
|
||||
import lombok.With;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.UUID;
|
||||
|
||||
@Value
|
||||
public class TerminalLaunchConfiguration {
|
||||
DataColor color;
|
||||
String coloredTitle;
|
||||
String cleanTitle;
|
||||
boolean preferTabs;
|
||||
|
||||
@With
|
||||
FilePath scriptFile;
|
||||
|
||||
ShellDialect scriptDialect;
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss").withZone(ZoneId.systemDefault());
|
||||
|
||||
public static TerminalLaunchConfiguration create(
|
||||
UUID request, DataStoreEntry entry, String cleanTitle, String adjustedTitle, boolean preferTabs
|
||||
) throws Exception {
|
||||
var color = entry != null ? DataStorage.get().getEffectiveColor(entry) : null;
|
||||
var d = ProcessControlProvider.get().getEffectiveLocalDialect();
|
||||
var launcherScript = d.terminalLauncherScript(request, adjustedTitle);
|
||||
var preparationScript = ScriptHelper.createLocalExecScript(launcherScript);
|
||||
|
||||
if (!AppPrefs.get().enableTerminalLogging().get()) {
|
||||
var config = new TerminalLaunchConfiguration(
|
||||
entry != null ? color : null, adjustedTitle, cleanTitle, preferTabs, preparationScript, d);
|
||||
return config;
|
||||
}
|
||||
|
||||
var feature = LicenseProvider.get().getFeature("logging");
|
||||
var supported = feature.isSupported();
|
||||
if (!supported) {
|
||||
throw new LicenseRequiredException(feature);
|
||||
}
|
||||
|
||||
var logDir = AppProperties.get().getDataDir().resolve("sessions");
|
||||
Files.createDirectories(logDir);
|
||||
var logFile = logDir.resolve(new FilePath(DataStorage.get().getStoreEntryDisplayName(entry) + " ("
|
||||
+ DATE_FORMATTER.format(Instant.now()) + ").log")
|
||||
.fileSystemCompatible(OsType.getLocal())
|
||||
.toString()
|
||||
.replaceAll(" ", "_"));
|
||||
try (var sc = LocalShell.getShell().start()) {
|
||||
if (OsType.getLocal() == OsType.WINDOWS) {
|
||||
var content =
|
||||
"""
|
||||
echo 'Transcript started, output file is "sessions\\%s"'
|
||||
Start-Transcript -Force -LiteralPath "%s" > $Out-Null
|
||||
& %s
|
||||
Stop-Transcript > $Out-Null
|
||||
echo 'Transcript stopped, output file is "sessions\\%s"'
|
||||
"""
|
||||
.formatted(
|
||||
logFile.getFileName().toString(),
|
||||
logFile,
|
||||
preparationScript,
|
||||
logFile.getFileName().toString());
|
||||
var ps = ScriptHelper.createExecScript(ShellDialects.POWERSHELL, sc, content);
|
||||
var config = new TerminalLaunchConfiguration(
|
||||
entry != null ? color : null, adjustedTitle, cleanTitle, preferTabs, ps, ShellDialects.POWERSHELL);
|
||||
return config;
|
||||
} else {
|
||||
var found = sc.command(sc.getShellDialect().getWhichCommand("script"))
|
||||
.executeAndCheck();
|
||||
if (!found) {
|
||||
var suffix = sc.getOsType() == OsType.MACOS
|
||||
? "This command is available in the util-linux package which can be installed via homebrew."
|
||||
: "This command is available in the util-linux package.";
|
||||
throw ErrorEvent.expected(new IllegalStateException(
|
||||
"Logging requires the script command to be installed. " + suffix));
|
||||
}
|
||||
|
||||
var content = sc.getOsType() == OsType.MACOS || sc.getOsType() == OsType.BSD
|
||||
? """
|
||||
echo "Transcript started, output file is sessions/%s"
|
||||
script -e -q "%s" "%s"
|
||||
echo "Transcript stopped, output file is sessions/%s"
|
||||
"""
|
||||
.formatted(logFile.getFileName(), logFile, preparationScript, logFile.getFileName())
|
||||
: """
|
||||
echo "Transcript started, output file is sessions/%s"
|
||||
script --quiet --command "%s" "%s"
|
||||
echo "Transcript stopped, output file is sessions/%s"
|
||||
"""
|
||||
.formatted(logFile.getFileName(), preparationScript, logFile, logFile.getFileName());
|
||||
var ps = ScriptHelper.createExecScript(sc.getShellDialect(), sc, content);
|
||||
var config = new TerminalLaunchConfiguration(
|
||||
entry != null ? color : null, adjustedTitle, cleanTitle, preferTabs, ps, sc.getShellDialect());
|
||||
return config;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public CommandBuilder getDialectLaunchCommand() {
|
||||
var open = scriptDialect.getOpenScriptCommand(scriptFile.toString());
|
||||
return open;
|
||||
}
|
||||
}
|
|
@ -1,25 +1,16 @@
|
|||
package io.xpipe.app.terminal;
|
||||
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.AppProperties;
|
||||
import io.xpipe.app.ext.ProcessControlProvider;
|
||||
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.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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
|
@ -41,24 +32,24 @@ public class TerminalLauncher {
|
|||
&& AppPrefs.get().clearTerminalOnInit().get(),
|
||||
TerminalInitFunction.none()),
|
||||
true);
|
||||
var config = new ExternalTerminalType.LaunchConfiguration(null, title, title, script, sc.getShellDialect());
|
||||
var config = new TerminalLaunchConfiguration(null, title, title, true, script, sc.getShellDialect());
|
||||
type.launch(config);
|
||||
}
|
||||
}
|
||||
|
||||
public static void open(String title, ProcessControl cc) throws Exception {
|
||||
open(null, title, null, cc, UUID.randomUUID());
|
||||
open(null, title, null, cc, UUID.randomUUID(), true);
|
||||
}
|
||||
|
||||
public static void open(String title, ProcessControl cc, UUID request) throws Exception {
|
||||
open(null, title, null, cc, request);
|
||||
open(null, title, null, cc, request, true);
|
||||
}
|
||||
|
||||
public static void open(DataStoreEntry entry, String title, String directory, ProcessControl cc) throws Exception {
|
||||
open(entry, title, directory, cc, UUID.randomUUID());
|
||||
open(entry, title, directory, cc, UUID.randomUUID(), true);
|
||||
}
|
||||
|
||||
public static void open(DataStoreEntry entry, String title, String directory, ProcessControl cc, UUID request)
|
||||
public static void open(DataStoreEntry entry, String title, String directory, ProcessControl cc, UUID request, boolean preferTabs)
|
||||
throws Exception {
|
||||
var type = AppPrefs.get().terminalType().getValue();
|
||||
if (type == null) {
|
||||
|
@ -76,7 +67,7 @@ public class TerminalLauncher {
|
|||
&& type.shouldClear()
|
||||
&& AppPrefs.get().clearTerminalOnInit().get(),
|
||||
cc instanceof ShellControl ? type.additionalInitCommands() : TerminalInitFunction.none());
|
||||
var config = createConfig(request, entry, cleanTitle, adjustedTitle);
|
||||
var config = TerminalLaunchConfiguration.create(request, entry, cleanTitle, adjustedTitle, preferTabs);
|
||||
var latch = TerminalLauncherManager.submitAsync(request, cc, terminalConfig, directory);
|
||||
try {
|
||||
type.launch(config);
|
||||
|
@ -89,84 +80,4 @@ public class TerminalLauncher {
|
|||
"Unable to launch terminal " + type.toTranslatedString().getValue() + ": " + modMsg, ex));
|
||||
}
|
||||
}
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss").withZone(ZoneId.systemDefault());
|
||||
|
||||
private static ExternalTerminalType.LaunchConfiguration createConfig(
|
||||
UUID request, DataStoreEntry entry, String cleanTitle, String adjustedTitle) throws Exception {
|
||||
var color = entry != null ? DataStorage.get().getEffectiveColor(entry) : null;
|
||||
var d = ProcessControlProvider.get().getEffectiveLocalDialect();
|
||||
var launcherScript = d.terminalLauncherScript(request, adjustedTitle);
|
||||
var preparationScript = ScriptHelper.createLocalExecScript(launcherScript);
|
||||
|
||||
if (!AppPrefs.get().enableTerminalLogging().get()) {
|
||||
var config = new ExternalTerminalType.LaunchConfiguration(
|
||||
entry != null ? color : null, adjustedTitle, cleanTitle, preparationScript, d);
|
||||
return config;
|
||||
}
|
||||
|
||||
var feature = LicenseProvider.get().getFeature("logging");
|
||||
var supported = feature.isSupported();
|
||||
if (!supported) {
|
||||
throw new LicenseRequiredException(feature);
|
||||
}
|
||||
|
||||
var logDir = AppProperties.get().getDataDir().resolve("sessions");
|
||||
Files.createDirectories(logDir);
|
||||
var logFile = logDir.resolve(new FilePath(DataStorage.get().getStoreEntryDisplayName(entry) + " ("
|
||||
+ DATE_FORMATTER.format(Instant.now()) + ").log")
|
||||
.fileSystemCompatible(OsType.getLocal())
|
||||
.toString()
|
||||
.replaceAll(" ", "_"));
|
||||
try (var sc = LocalShell.getShell().start()) {
|
||||
if (OsType.getLocal() == OsType.WINDOWS) {
|
||||
var content =
|
||||
"""
|
||||
echo 'Transcript started, output file is "sessions\\%s"'
|
||||
Start-Transcript -Force -LiteralPath "%s" > $Out-Null
|
||||
& %s
|
||||
Stop-Transcript > $Out-Null
|
||||
echo 'Transcript stopped, output file is "sessions\\%s"'
|
||||
"""
|
||||
.formatted(
|
||||
logFile.getFileName().toString(),
|
||||
logFile,
|
||||
preparationScript,
|
||||
logFile.getFileName().toString());
|
||||
var ps = ScriptHelper.createExecScript(ShellDialects.POWERSHELL, sc, content);
|
||||
var config = new ExternalTerminalType.LaunchConfiguration(
|
||||
entry != null ? color : null, adjustedTitle, cleanTitle, ps, ShellDialects.POWERSHELL);
|
||||
return config;
|
||||
} else {
|
||||
var found = sc.command(sc.getShellDialect().getWhichCommand("script"))
|
||||
.executeAndCheck();
|
||||
if (!found) {
|
||||
var suffix = sc.getOsType() == OsType.MACOS
|
||||
? "This command is available in the util-linux package which can be installed via homebrew."
|
||||
: "This command is available in the util-linux package.";
|
||||
throw ErrorEvent.expected(new IllegalStateException(
|
||||
"Logging requires the script command to be installed. " + suffix));
|
||||
}
|
||||
|
||||
var content = sc.getOsType() == OsType.MACOS || sc.getOsType() == OsType.BSD
|
||||
? """
|
||||
echo "Transcript started, output file is sessions/%s"
|
||||
script -e -q "%s" "%s"
|
||||
echo "Transcript stopped, output file is sessions/%s"
|
||||
"""
|
||||
.formatted(logFile.getFileName(), logFile, preparationScript, logFile.getFileName())
|
||||
: """
|
||||
echo "Transcript started, output file is sessions/%s"
|
||||
script --quiet --command "%s" "%s"
|
||||
echo "Transcript stopped, output file is sessions/%s"
|
||||
"""
|
||||
.formatted(logFile.getFileName(), preparationScript, logFile, logFile.getFileName());
|
||||
var ps = ScriptHelper.createExecScript(sc.getShellDialect(), sc, content);
|
||||
var config = new ExternalTerminalType.LaunchConfiguration(
|
||||
entry != null ? color : null, adjustedTitle, cleanTitle, ps, sc.getShellDialect());
|
||||
return config;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,9 +18,6 @@ public class TerminalLauncherManager {
|
|||
|
||||
public static void init() {
|
||||
TerminalView.get().addListener(new TerminalView.Listener() {
|
||||
@Override
|
||||
public void onSessionOpened(TerminalView.ShellSession session) {}
|
||||
|
||||
@Override
|
||||
public void onSessionClosed(TerminalView.ShellSession session) {
|
||||
var affectedEntry = entries.values().stream()
|
||||
|
@ -34,12 +31,6 @@ public class TerminalLauncherManager {
|
|||
|
||||
affectedEntry.get().abort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminalOpened(TerminalView.TerminalSession instance) {}
|
||||
|
||||
@Override
|
||||
public void onTerminalClosed(TerminalView.TerminalSession instance) {}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
package io.xpipe.app.terminal;
|
||||
|
||||
public enum TerminalOpenFormat {
|
||||
|
||||
NEW_WINDOW,
|
||||
TABBED,
|
||||
NEW_WINDOW_OR_TABBED;
|
||||
}
|
|
@ -26,7 +26,7 @@ public class TerminalView {
|
|||
public static class ShellSession {
|
||||
UUID request;
|
||||
ProcessHandle shell;
|
||||
ProcessHandle terminal;
|
||||
TerminalSession terminal;
|
||||
}
|
||||
|
||||
@Getter
|
||||
|
@ -38,6 +38,10 @@ public class TerminalView {
|
|||
this.terminalProcess = terminalProcess;
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return terminalProcess.isAlive();
|
||||
}
|
||||
|
||||
public Optional<ControllableTerminalSession> controllable() {
|
||||
return Optional.ofNullable(this instanceof ControllableTerminalSession c ? c : null);
|
||||
}
|
||||
|
@ -45,13 +49,13 @@ public class TerminalView {
|
|||
|
||||
public static interface Listener {
|
||||
|
||||
void onSessionOpened(ShellSession session);
|
||||
default void onSessionOpened(ShellSession session) {};
|
||||
|
||||
void onSessionClosed(ShellSession session);
|
||||
default void onSessionClosed(ShellSession session) {};
|
||||
|
||||
void onTerminalOpened(TerminalSession instance);
|
||||
default void onTerminalOpened(TerminalSession instance) {};
|
||||
|
||||
void onTerminalClosed(TerminalSession instance);
|
||||
default void onTerminalClosed(TerminalSession instance) {};
|
||||
}
|
||||
|
||||
private final List<ShellSession> sessions = new ArrayList<>();
|
||||
|
@ -97,20 +101,17 @@ public class TerminalView {
|
|||
return;
|
||||
}
|
||||
|
||||
var session = new ShellSession(request, shell.get(), terminal.get());
|
||||
var instance = terminalInstances.stream()
|
||||
.filter(i -> i.getTerminalProcess().equals(terminal.get()))
|
||||
.findFirst();
|
||||
if (instance.isEmpty()) {
|
||||
var tv = createTerminalSession(terminal.get());
|
||||
if (tv.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
var tv = createTerminalSession(terminal.get());
|
||||
if (tv.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!terminalInstances.contains(tv.get())) {
|
||||
terminalInstances.add(tv.get());
|
||||
listeners.forEach(listener -> listener.onTerminalOpened(tv.get()));
|
||||
}
|
||||
|
||||
var session = new ShellSession(request, shell.get(), tv.get());
|
||||
sessions.add(session);
|
||||
listeners.forEach(listener -> listener.onSessionOpened(session));
|
||||
|
||||
|
@ -124,12 +125,18 @@ public class TerminalView {
|
|||
case OsType.Linux linux -> Optional.of(new TerminalSession(terminalProcess));
|
||||
case OsType.MacOs macOs -> Optional.of(new TerminalSession(terminalProcess));
|
||||
case OsType.Windows windows -> {
|
||||
var control = NativeWinWindowControl.byPid(terminalProcess.pid());
|
||||
if (control.isEmpty()) {
|
||||
var controls = NativeWinWindowControl.byPid(terminalProcess.pid());
|
||||
if (controls.isEmpty()) {
|
||||
yield Optional.empty();
|
||||
}
|
||||
|
||||
yield Optional.of(new WindowsTerminalSession(terminalProcess, control.get()));
|
||||
var existing = terminalInstances.stream().map(terminalSession -> ((WindowsTerminalSession) terminalSession).getControl()).toList();
|
||||
controls.removeAll(existing);
|
||||
if (controls.isEmpty()) {
|
||||
yield Optional.empty();
|
||||
}
|
||||
|
||||
yield Optional.of(new WindowsTerminalSession(terminalProcess, controls.getFirst()));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -176,7 +183,7 @@ public class TerminalView {
|
|||
|
||||
public synchronized void tick() {
|
||||
for (ShellSession session : new ArrayList<>(sessions)) {
|
||||
var alive = session.shell.isAlive() && session.terminal.isAlive();
|
||||
var alive = session.shell.isAlive() && session.getTerminal().isRunning();
|
||||
if (!alive) {
|
||||
sessions.remove(session);
|
||||
listeners.forEach(listener -> listener.onSessionClosed(session));
|
||||
|
@ -184,7 +191,7 @@ public class TerminalView {
|
|||
}
|
||||
|
||||
for (TerminalSession terminalInstance : new ArrayList<>(terminalInstances)) {
|
||||
var alive = terminalInstance.getTerminalProcess().isAlive();
|
||||
var alive = terminalInstance.isRunning();
|
||||
if (!alive) {
|
||||
terminalInstances.remove(terminalInstance);
|
||||
TrackEvent.withTrace("Terminal session is dead")
|
||||
|
|
108
app/src/main/java/io/xpipe/app/terminal/TermiusTerminalType.java
Normal file
108
app/src/main/java/io/xpipe/app/terminal/TermiusTerminalType.java
Normal file
|
@ -0,0 +1,108 @@
|
|||
package io.xpipe.app.terminal;
|
||||
|
||||
import io.xpipe.app.comp.base.MarkdownComp;
|
||||
import io.xpipe.app.core.AppCache;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.window.AppWindowHelper;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.util.Hyperlinks;
|
||||
import io.xpipe.app.util.LocalShell;
|
||||
import io.xpipe.app.util.SshLocalBridge;
|
||||
import io.xpipe.app.util.WindowsRegistry;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonBar;
|
||||
import javafx.scene.control.ButtonType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class TermiusTerminalType implements ExternalTerminalType {
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "app.termius";
|
||||
}
|
||||
|
||||
@Override
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.TABBED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
try (var sc = LocalShell.getShell()) {
|
||||
return switch (OsType.getLocal()) {
|
||||
case OsType.Linux linux -> {
|
||||
yield Files.exists(Path.of("/opt/Termius"));
|
||||
}
|
||||
case OsType.MacOs macOs -> {
|
||||
yield Files.exists(Path.of("/Applications/Termius.app"));
|
||||
}
|
||||
case OsType.Windows windows -> {
|
||||
var r = WindowsRegistry.local().readValue(WindowsRegistry.HKEY_CURRENT_USER, "SOFTWARE\\Classes\\termius");
|
||||
yield r.isPresent();
|
||||
}
|
||||
};
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).omit().handle();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWebsite() {
|
||||
return "https://termius.com/";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecommended() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsColoredTitle() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
SshLocalBridge.init();
|
||||
if (!showInfo()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var host = "localhost";
|
||||
var b = SshLocalBridge.get();
|
||||
var port = b.getPort();
|
||||
var user = b.getUser();
|
||||
var name = b.getIdentityKey().getFileName().toString();
|
||||
Hyperlinks.open("termius://app/host-sharing#label=" + name + "&ip=" + host + "&port=" + port + "&username=" + user + "&os=undefined");
|
||||
}
|
||||
|
||||
private boolean showInfo() throws IOException {
|
||||
boolean set = AppCache.getBoolean("termiusSetup", false);
|
||||
if (set) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var b = SshLocalBridge.get();
|
||||
var keyContent = Files.readString(b.getIdentityKey());
|
||||
var r = AppWindowHelper.showBlockingAlert(alert -> {
|
||||
alert.setTitle(AppI18n.get("termiusSetup"));
|
||||
alert.setAlertType(Alert.AlertType.NONE);
|
||||
|
||||
var activated = AppI18n.get().getMarkdownDocumentation("app:termiusSetup").formatted(b.getIdentityKey(), keyContent);
|
||||
var markdown = new MarkdownComp(activated, s -> s).prefWidth(450).prefHeight(450).createRegion();
|
||||
alert.getDialogPane().setContent(markdown);
|
||||
|
||||
alert.getButtonTypes().add(new ButtonType(AppI18n.get("ok"), ButtonBar.ButtonData.OK_DONE));
|
||||
});
|
||||
r.filter(buttonType -> buttonType.getButtonData().isDefaultButton());
|
||||
r.ifPresent(buttonType -> {
|
||||
AppCache.update("termiusSetup", true);
|
||||
});
|
||||
return r.isPresent();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package io.xpipe.app.terminal;
|
||||
|
||||
import io.xpipe.app.util.LocalShell;
|
||||
import io.xpipe.core.process.CommandBuilder;
|
||||
import io.xpipe.core.process.ShellDialect;
|
||||
import io.xpipe.core.process.ShellDialects;
|
||||
import io.xpipe.core.process.TerminalInitFunction;
|
||||
import io.xpipe.core.util.FailableFunction;
|
||||
|
||||
public class WarpTerminalType extends ExternalTerminalType.MacOsType {
|
||||
|
||||
public WarpTerminalType() {super("app.warp", "Warp");}
|
||||
|
||||
@Override
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.TABBED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProcessHierarchyOffset() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWebsite() {
|
||||
return "https://www.warp.dev/";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecommended() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsColoredTitle() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldClear() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
LocalShell.getShell().executeSimpleCommand(CommandBuilder.of()
|
||||
.add("open", "-a")
|
||||
.addQuoted("Warp.app")
|
||||
.addFile(configuration.getScriptFile()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public FailableFunction<TerminalLaunchConfiguration, String, Exception> remoteLaunchCommand(
|
||||
ShellDialect systemDialect
|
||||
) {
|
||||
return launchConfiguration -> {
|
||||
var toExecute = CommandBuilder.of().add("open", "-a").addQuoted("Warp.app").addFile(launchConfiguration.getScriptFile());
|
||||
return toExecute.buildSimple();
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public TerminalInitFunction additionalInitCommands() {
|
||||
return TerminalInitFunction.of(sc -> {
|
||||
if (sc.getShellDialect() == ShellDialects.ZSH) {
|
||||
return "printf '\\eP$f{\"hook\": \"SourcedRcFileForWarp\", \"value\": { \"shell\": \"zsh\"}}\\x9c'";
|
||||
}
|
||||
if (sc.getShellDialect() == ShellDialects.BASH) {
|
||||
return "printf '\\eP$f{\"hook\": \"SourcedRcFileForWarp\", \"value\": { \"shell\": \"bash\"}}\\x9c'";
|
||||
}
|
||||
if (sc.getShellDialect() == ShellDialects.FISH) {
|
||||
return "printf '\\eP$f{\"hook\": \"SourcedRcFileForWarp\", \"value\": { \"shell\": \"fish\"}}\\x9c'";
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -18,11 +18,6 @@ public interface WezTerminalType extends ExternalTerminalType, TrackableTerminal
|
|||
ExternalTerminalType WEZTERM_LINUX = new Linux();
|
||||
ExternalTerminalType WEZTERM_MAC_OS = new MacOs();
|
||||
|
||||
@Override
|
||||
default boolean supportsTabs() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
default String getWebsite() {
|
||||
return "https://wezfurlong.org/wezterm/index.html";
|
||||
|
@ -45,7 +40,12 @@ public interface WezTerminalType extends ExternalTerminalType, TrackableTerminal
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void execute(Path file, LaunchConfiguration configuration) throws Exception {
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.NEW_WINDOW;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(Path file, TerminalLaunchConfiguration configuration) throws Exception {
|
||||
LocalShell.getShell()
|
||||
.executeSimpleCommand(CommandBuilder.of()
|
||||
.addFile(file.toString())
|
||||
|
@ -90,6 +90,11 @@ public interface WezTerminalType extends ExternalTerminalType, TrackableTerminal
|
|||
super("app.wezterm");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.TABBED;
|
||||
}
|
||||
|
||||
public boolean isAvailable() {
|
||||
try (ShellControl pc = LocalShell.getShell()) {
|
||||
return pc.executeSimpleBooleanCommand(pc.getShellDialect().getWhichCommand("wezterm"))
|
||||
|
@ -101,7 +106,7 @@ public interface WezTerminalType extends ExternalTerminalType, TrackableTerminal
|
|||
}
|
||||
|
||||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
var spawn = LocalShell.getShell()
|
||||
.command(CommandBuilder.of()
|
||||
.addFile("wezterm")
|
||||
|
@ -122,7 +127,12 @@ public interface WezTerminalType extends ExternalTerminalType, TrackableTerminal
|
|||
}
|
||||
|
||||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.TABBED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
try (var sc = LocalShell.getShell()) {
|
||||
var pathOut = sc.command(String.format(
|
||||
"mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications 2>/dev/null",
|
||||
|
|
|
@ -4,9 +4,11 @@ import io.xpipe.app.core.window.NativeWinWindowControl;
|
|||
import io.xpipe.app.util.Rect;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
@Getter
|
||||
public final class WindowsTerminalSession extends ControllableTerminalSession {
|
||||
|
||||
NativeWinWindowControl control;
|
||||
|
@ -16,6 +18,11 @@ public final class WindowsTerminalSession extends ControllableTerminalSession {
|
|||
this.control = control;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return super.isRunning() && control.isVisible();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
this.control.show();
|
||||
|
|
|
@ -18,8 +18,8 @@ public interface WindowsTerminalType extends ExternalTerminalType, TrackableTerm
|
|||
ExternalTerminalType WINDOWS_TERMINAL_PREVIEW = new Preview();
|
||||
ExternalTerminalType WINDOWS_TERMINAL_CANARY = new Canary();
|
||||
|
||||
private static CommandBuilder toCommand(ExternalTerminalType.LaunchConfiguration configuration) throws Exception {
|
||||
var cmd = CommandBuilder.of().add("-w", "1", "nt");
|
||||
private static CommandBuilder toCommand(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
var cmd = CommandBuilder.of().addIf(configuration.isPreferTabs(), "-w", "1").add("nt");
|
||||
|
||||
if (configuration.getColor() != null) {
|
||||
cmd.add("--tabColor").addQuoted(configuration.getColor().toHexString());
|
||||
|
@ -56,6 +56,11 @@ public interface WindowsTerminalType extends ExternalTerminalType, TrackableTerm
|
|||
return cmd;
|
||||
}
|
||||
|
||||
@Override
|
||||
default TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.NEW_WINDOW_OR_TABBED;
|
||||
}
|
||||
|
||||
@Override
|
||||
default int getProcessHierarchyOffset() {
|
||||
var powershell = AppPrefs.get().enableTerminalLogging().get()
|
||||
|
@ -63,11 +68,6 @@ public interface WindowsTerminalType extends ExternalTerminalType, TrackableTerm
|
|||
return powershell ? 1 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
default boolean supportsTabs() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
default boolean isRecommended() {
|
||||
return true;
|
||||
|
@ -90,7 +90,7 @@ public interface WindowsTerminalType extends ExternalTerminalType, TrackableTerm
|
|||
}
|
||||
|
||||
@Override
|
||||
protected CommandBuilder toCommand(LaunchConfiguration configuration) throws Exception {
|
||||
protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
return WindowsTerminalType.toCommand(configuration);
|
||||
}
|
||||
}
|
||||
|
@ -103,9 +103,9 @@ public interface WindowsTerminalType extends ExternalTerminalType, TrackableTerm
|
|||
}
|
||||
|
||||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
if (!isAvailable()) {
|
||||
throw ErrorEvent.expected(new IllegalArgumentException("Windows Terminal Preview is not installed"));
|
||||
throw ErrorEvent.expected(new IllegalArgumentException("Windows Terminal Preview is not installed at " + getPath()));
|
||||
}
|
||||
|
||||
LocalShell.getShell()
|
||||
|
@ -138,9 +138,9 @@ public interface WindowsTerminalType extends ExternalTerminalType, TrackableTerm
|
|||
}
|
||||
|
||||
@Override
|
||||
public void launch(LaunchConfiguration configuration) throws Exception {
|
||||
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
|
||||
if (!isAvailable()) {
|
||||
throw ErrorEvent.expected(new IllegalArgumentException("Windows Terminal Canary is not installed"));
|
||||
throw ErrorEvent.expected(new IllegalArgumentException("Windows Terminal Canary is not installed at " + getPath()));
|
||||
}
|
||||
|
||||
LocalShell.getShell()
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
package io.xpipe.app.terminal;
|
||||
|
||||
import io.xpipe.app.comp.base.MarkdownComp;
|
||||
import io.xpipe.app.core.AppCache;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.window.AppWindowHelper;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.util.LocalShell;
|
||||
import io.xpipe.app.util.SshLocalBridge;
|
||||
import io.xpipe.app.util.WindowsRegistry;
|
||||
import io.xpipe.core.process.CommandBuilder;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonBar;
|
||||
import javafx.scene.control.ButtonType;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
public class XShellTerminalType extends ExternalTerminalType.WindowsType {
|
||||
|
||||
public XShellTerminalType() {super("app.xShell", "Xshell");}
|
||||
|
||||
@Override
|
||||
public TerminalOpenFormat getOpenFormat() {
|
||||
return TerminalOpenFormat.TABBED;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<Path> determineInstallation() {
|
||||
try {
|
||||
var r = WindowsRegistry.local().readValue(WindowsRegistry.HKEY_LOCAL_MACHINE,
|
||||
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\Xshell.exe");
|
||||
return r.map(Path::of);
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).omit().handle();
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWebsite() {
|
||||
return "https://www.netsarang.com/en/xshell/";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRecommended() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsColoredTitle() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(Path file, TerminalLaunchConfiguration configuration) throws Exception {
|
||||
SshLocalBridge.init();
|
||||
if (!showInfo()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (var sc = LocalShell.getShell()) {
|
||||
var b = SshLocalBridge.get();
|
||||
var keyName = b.getIdentityKey().getFileName().toString();
|
||||
var command = CommandBuilder.of()
|
||||
.addFile(file.toString())
|
||||
.add("-url")
|
||||
.addQuoted("ssh://" + b.getUser() + "@localhost:" + b.getPort())
|
||||
.add("-i", keyName);
|
||||
sc.executeSimpleCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean showInfo() {
|
||||
boolean set = AppCache.getBoolean("xshellSetup", false);
|
||||
if (set) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var b = SshLocalBridge.get();
|
||||
var keyName = b.getIdentityKey().getFileName().toString();
|
||||
var r = AppWindowHelper.showBlockingAlert(alert -> {
|
||||
alert.setTitle(AppI18n.get("xshellSetup"));
|
||||
alert.setAlertType(Alert.AlertType.NONE);
|
||||
|
||||
var activated = AppI18n.get().getMarkdownDocumentation("app:xshellSetup").formatted(b.getIdentityKey(), keyName);
|
||||
var markdown = new MarkdownComp(activated, s -> s).prefWidth(450).prefHeight(400).createRegion();
|
||||
alert.getDialogPane().setContent(markdown);
|
||||
|
||||
alert.getButtonTypes().add(new ButtonType(AppI18n.get("ok"), ButtonBar.ButtonData.OK_DONE));
|
||||
});
|
||||
r.filter(buttonType -> buttonType.getButtonData().isDefaultButton());
|
||||
r.ifPresent(buttonType -> {
|
||||
AppCache.update("xshellSetup", true);
|
||||
});
|
||||
return r.isPresent();
|
||||
}
|
||||
}
|
|
@ -30,22 +30,16 @@ public abstract class MultiExecuteAction implements BrowserBranchAction {
|
|||
model.withShell(
|
||||
pc -> {
|
||||
for (BrowserEntry entry : entries) {
|
||||
var cmd = pc.command(createCommand(pc, model, entry));
|
||||
if (cmd == null) {
|
||||
var c = createCommand(pc, model, entry);
|
||||
if (c == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var uuid = UUID.randomUUID();
|
||||
model.getTerminalRequests().add(uuid);
|
||||
TerminalLauncher.open(
|
||||
model.getEntry().getEntry(),
|
||||
entry.getRawFileEntry().getName(),
|
||||
model.getCurrentDirectory() != null
|
||||
? model.getCurrentDirectory()
|
||||
.getPath()
|
||||
: null,
|
||||
cmd,
|
||||
uuid);
|
||||
var cmd = pc.command(c);
|
||||
model.openTerminalAsync(entry.getRawFileEntry().getName(), model.getCurrentDirectory() != null
|
||||
? model.getCurrentDirectory()
|
||||
.getPath()
|
||||
: null, cmd);
|
||||
}
|
||||
},
|
||||
false);
|
||||
|
|
|
@ -34,18 +34,16 @@ public abstract class MultiExecuteSelectionAction implements BrowserBranchAction
|
|||
public void execute(BrowserFileSystemTabModel 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(),
|
||||
getTerminalTitle(),
|
||||
model.getCurrentDirectory() != null
|
||||
? model.getCurrentDirectory()
|
||||
.getPath()
|
||||
: null,
|
||||
cmd,
|
||||
uuid);
|
||||
var c = createCommand(pc, model, entries);
|
||||
if (c == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var cmd = pc.command(c);
|
||||
model.openTerminalAsync(getTerminalTitle(), model.getCurrentDirectory() != null
|
||||
? model.getCurrentDirectory()
|
||||
.getPath()
|
||||
: null, cmd);
|
||||
},
|
||||
false);
|
||||
}
|
||||
|
|
|
@ -17,33 +17,20 @@ import javafx.scene.input.KeyCombination;
|
|||
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class OpenTerminalAction implements BrowserLeafAction {
|
||||
|
||||
@Override
|
||||
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
|
||||
if (entries.size() == 0) {
|
||||
model.openTerminalAsync(
|
||||
model.getCurrentDirectory() != null
|
||||
? model.getCurrentDirectory().getPath()
|
||||
: null);
|
||||
} else {
|
||||
for (var entry : entries) {
|
||||
model.openTerminalAsync(entry.getRawFileEntry().getPath());
|
||||
}
|
||||
}
|
||||
|
||||
if (AppPrefs.get().enableTerminalDocking().get()
|
||||
&& model.getBrowserModel() instanceof BrowserFullSessionModel sessionModel) {
|
||||
// Check if the right side is already occupied
|
||||
var existingSplit = sessionModel.getSplits().get(model);
|
||||
if (existingSplit != null && !(existingSplit instanceof BrowserTerminalDockTabModel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sessionModel.splitTab(
|
||||
model, new BrowserTerminalDockTabModel(sessionModel, model, model.getTerminalRequests()));
|
||||
var dirs = entries.size() > 0 ? entries.stream().map(browserEntry -> browserEntry.getRawFileEntry().getPath()).toList() : model.getCurrentDirectory() != null
|
||||
? List.of(model.getCurrentDirectory().getPath())
|
||||
: Collections.singletonList((String) null);
|
||||
for (String dir : dirs) {
|
||||
var name = (dir != null ? dir + " - " : "") + model.getName();
|
||||
model.openTerminalAsync(name, dir, model.getFileSystem().getShell().orElseThrow());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package io.xpipe.ext.base.desktop;
|
|||
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.terminal.ExternalTerminalType;
|
||||
import io.xpipe.app.terminal.TerminalLaunchConfiguration;
|
||||
import io.xpipe.app.util.Validators;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.process.ShellDialect;
|
||||
|
@ -99,7 +100,7 @@ public class DesktopEnvironmentStore extends JacksonizedValue
|
|||
.createScript(
|
||||
dialect,
|
||||
dialect.prepareTerminalInitFileOpenCommand(dialect, null, scriptFile.toString(), false));
|
||||
var launchConfig = new ExternalTerminalType.LaunchConfiguration(null, name, name, launchScriptFile, dialect);
|
||||
var launchConfig = new TerminalLaunchConfiguration(null, name, name, true, launchScriptFile, dialect);
|
||||
base.getStore().runDesktopScript(name, launchCommand.apply(launchConfig));
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue