diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java index 6a69d3f65..35cf9b5ea 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileSystemTabModel.java @@ -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 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 cdSyncOrRetry(String path, boolean customInput) { if (Objects.equals(path, currentPath.get())) { return Optional.empty(); @@ -250,27 +276,15 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab 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 { if (fileSystem == null) { return; @@ -533,12 +547,14 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab { 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(); diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserTerminalDockTabModel.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserTerminalDockTabModel.java index 0053f7e57..b295026dd 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserTerminalDockTabModel.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserTerminalDockTabModel.java @@ -49,8 +49,6 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab { @Override public void init() throws Exception { - var sessions = new ArrayList(); - var terminals = new ArrayList(); 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); } } diff --git a/app/src/main/java/io/xpipe/app/core/window/NativeWinWindowControl.java b/app/src/main/java/io/xpipe/app/core/window/NativeWinWindowControl.java index 70b81c30f..bcde86cf9 100644 --- a/app/src/main/java/io/xpipe/app/core/window/NativeWinWindowControl.java +++ b/app/src/main/java/io/xpipe/app/core/window/NativeWinWindowControl.java @@ -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 byPid(long pid) { - var ref = new AtomicReference(); + public static List byPid(long pid) { + var refs = new ArrayList(); 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; diff --git a/app/src/main/java/io/xpipe/app/terminal/AlacrittyTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/AlacrittyTerminalType.java index 7611a17cf..9f220ddf9 100644 --- a/app/src/main/java/io/xpipe/app/terminal/AlacrittyTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/AlacrittyTerminalType.java @@ -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") diff --git a/app/src/main/java/io/xpipe/app/terminal/CmdTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/CmdTerminalType.java index 7ef420f9d..8b2a2dcb0 100644 --- a/app/src/main/java/io/xpipe/app/terminal/CmdTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/CmdTerminalType.java @@ -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()); } diff --git a/app/src/main/java/io/xpipe/app/terminal/CustomTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/CustomTerminalType.java index 170e443f0..a239590be 100644 --- a/app/src/main/java/io/xpipe/app/terminal/CustomTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/CustomTerminalType.java @@ -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")); diff --git a/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java index ed8b7357a..db24fca56 100644 --- a/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java @@ -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 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 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 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 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 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 remoteLaunchCommand(ShellDialect systemDialect) { + default FailableFunction 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 remoteLaunchCommand( + public FailableFunction 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; } + } diff --git a/app/src/main/java/io/xpipe/app/terminal/GnomeTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/GnomeTerminalType.java index c12576c4b..91a344d6a 100644 --- a/app/src/main/java/io/xpipe/app/terminal/GnomeTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/GnomeTerminalType.java @@ -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 remoteLaunchCommand(ShellDialect systemDialect) { + public FailableFunction remoteLaunchCommand(ShellDialect systemDialect) { return launchConfiguration -> { var toExecute = CommandBuilder.of() .add(executable, "-v", "--title") diff --git a/app/src/main/java/io/xpipe/app/terminal/KittyTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/KittyTerminalType.java index e8e2c2a51..8c0e1f20c 100644 --- a/app/src/main/java/io/xpipe/app/terminal/KittyTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/KittyTerminalType.java @@ -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()) { diff --git a/app/src/main/java/io/xpipe/app/terminal/MobaXTermTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/MobaXTermTerminalType.java new file mode 100644 index 000000000..edc01c31f --- /dev/null +++ b/app/src/main/java/io/xpipe/app/terminal/MobaXTermTerminalType.java @@ -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 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(); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/terminal/PowerShellTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/PowerShellTerminalType.java index 55d57dc40..cfa267892 100644 --- a/app/src/main/java/io/xpipe/app/terminal/PowerShellTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/PowerShellTerminalType.java @@ -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") diff --git a/app/src/main/java/io/xpipe/app/terminal/PwshTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/PwshTerminalType.java index dbe253dbc..2d3703c08 100644 --- a/app/src/main/java/io/xpipe/app/terminal/PwshTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/PwshTerminalType.java @@ -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") diff --git a/app/src/main/java/io/xpipe/app/terminal/SecureCrtTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/SecureCrtTerminalType.java new file mode 100644 index 000000000..b08e9edba --- /dev/null +++ b/app/src/main/java/io/xpipe/app/terminal/SecureCrtTerminalType.java @@ -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 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); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/terminal/TabbyTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/TabbyTerminalType.java index f89561afe..71f62fce2 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TabbyTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/TabbyTerminalType.java @@ -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") diff --git a/app/src/main/java/io/xpipe/app/terminal/TerminalLaunchConfiguration.java b/app/src/main/java/io/xpipe/app/terminal/TerminalLaunchConfiguration.java new file mode 100644 index 000000000..c44d13b86 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalLaunchConfiguration.java @@ -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; + } +} diff --git a/app/src/main/java/io/xpipe/app/terminal/TerminalLauncher.java b/app/src/main/java/io/xpipe/app/terminal/TerminalLauncher.java index eb8c144ab..52ad4b8a9 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalLauncher.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalLauncher.java @@ -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; - } - } - } } diff --git a/app/src/main/java/io/xpipe/app/terminal/TerminalLauncherManager.java b/app/src/main/java/io/xpipe/app/terminal/TerminalLauncherManager.java index f7b467e57..0ee914dc1 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalLauncherManager.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalLauncherManager.java @@ -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) {} }); } diff --git a/app/src/main/java/io/xpipe/app/terminal/TerminalOpenFormat.java b/app/src/main/java/io/xpipe/app/terminal/TerminalOpenFormat.java new file mode 100644 index 000000000..aa217d735 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalOpenFormat.java @@ -0,0 +1,8 @@ +package io.xpipe.app.terminal; + +public enum TerminalOpenFormat { + + NEW_WINDOW, + TABBED, + NEW_WINDOW_OR_TABBED; +} diff --git a/app/src/main/java/io/xpipe/app/terminal/TerminalView.java b/app/src/main/java/io/xpipe/app/terminal/TerminalView.java index 709a77f98..225d231d9 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalView.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalView.java @@ -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 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 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") diff --git a/app/src/main/java/io/xpipe/app/terminal/TermiusTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/TermiusTerminalType.java new file mode 100644 index 000000000..33d5810af --- /dev/null +++ b/app/src/main/java/io/xpipe/app/terminal/TermiusTerminalType.java @@ -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(); + } +} diff --git a/app/src/main/java/io/xpipe/app/terminal/WarpTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/WarpTerminalType.java new file mode 100644 index 000000000..8748a1994 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/terminal/WarpTerminalType.java @@ -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 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; + }); + } +} diff --git a/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java index cf00e4337..a4e94bdbd 100644 --- a/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java @@ -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", diff --git a/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalSession.java b/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalSession.java index 70c447d7a..9659be7f3 100644 --- a/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalSession.java +++ b/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalSession.java @@ -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(); diff --git a/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalType.java index b0a153bda..cce15eaa2 100644 --- a/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalType.java @@ -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() diff --git a/app/src/main/java/io/xpipe/app/terminal/XShellTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/XShellTerminalType.java new file mode 100644 index 000000000..954c836c1 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/terminal/XShellTerminalType.java @@ -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 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(); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/MultiExecuteAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/MultiExecuteAction.java index f4e4b6caa..5c742cd17 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/MultiExecuteAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/MultiExecuteAction.java @@ -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); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/MultiExecuteSelectionAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/MultiExecuteSelectionAction.java index 2a7c1b947..5fcd874e8 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/MultiExecuteSelectionAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/MultiExecuteSelectionAction.java @@ -34,18 +34,16 @@ public abstract class MultiExecuteSelectionAction implements BrowserBranchAction public void execute(BrowserFileSystemTabModel model, List 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); } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java index 9006791a2..3ba0c55d8 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/OpenTerminalAction.java @@ -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 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()); } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopEnvironmentStore.java b/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopEnvironmentStore.java index 75f755e5d..7dd72435d 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopEnvironmentStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopEnvironmentStore.java @@ -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)); }