Terminal rework

This commit is contained in:
crschnick 2024-11-14 14:58:48 +00:00
parent 7fd07242f5
commit b350222769
29 changed files with 825 additions and 764 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,8 @@
package io.xpipe.app.terminal;
public enum TerminalOpenFormat {
NEW_WINDOW,
TABBED,
NEW_WINDOW_OR_TABBED;
}

View file

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

View 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();
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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