From 6018a7bf76cedbcb0043ca97746f992238b68db9 Mon Sep 17 00:00:00 2001 From: crschnick Date: Mon, 2 Jun 2025 19:28:21 +0000 Subject: [PATCH] Rework --- .../java/io/xpipe/app/core/mode/BaseMode.java | 2 +- .../java/io/xpipe/app/prefs/AppPrefs.java | 16 +- .../app/prefs/ExternalApplicationType.java | 104 ++--- .../xpipe/app/prefs/ExternalEditorType.java | 278 +++++++++--- .../app/prefs/ExternalRdpClientType.java | 412 ------------------ .../app/prefs/PasswordManagerCategory.java | 2 +- .../java/io/xpipe/app/prefs/RdpCategory.java | 5 +- .../BitwardenPasswordManager.java | 2 +- .../DashlanePasswordManager.java | 2 +- .../EnpassPasswordManager.java | 6 +- .../KeePassXcAssociationKey.java | 2 +- .../KeePassXcPasswordManager.java | 2 +- .../KeePassXcProxyClient.java | 2 +- .../KeeperPasswordManager.java | 2 +- .../LastpassPasswordManager.java | 2 +- .../OnePasswordManager.java | 2 +- .../{password => pwman}/PasswordManager.java | 2 +- .../PasswordManagerCommand.java | 3 +- .../PasswordManagerCommandTemplate.java | 2 +- .../PsonoPasswordManager.java | 2 +- .../{password => pwman}/TweetNaClHelper.java | 2 +- .../WindowsCredentialManager.java | 2 +- .../io/xpipe/app/rdp/CustomRdpClient.java | 40 ++ .../xpipe/app/rdp/DevolutionsRdpClient.java | 53 +++ .../io/xpipe/app/rdp/ExternalRdpClient.java | 93 ++++ .../java/io/xpipe/app/rdp/FreeRdpClient.java | 38 ++ .../java/io/xpipe/app/rdp/MstscRdpClient.java | 75 ++++ .../io/xpipe/app/rdp/RdpLaunchConfig.java | 15 + .../io/xpipe/app/rdp/RemminaRdpClient.java | 113 +++++ .../app/rdp/RemoteDesktopAppRdpClient.java | 32 ++ .../io/xpipe/app/rdp/WindowsAppRdpClient.java | 29 ++ .../app/terminal/AlacrittyTerminalType.java | 92 +++- .../xpipe/app/terminal/CmdTerminalType.java | 32 +- .../app/terminal/CustomTerminalType.java | 11 +- .../app/terminal/ExternalTerminalType.java | 132 +----- .../xpipe/app/terminal/GnomeConsoleType.java | 26 +- .../xpipe/app/terminal/GnomeTerminalType.java | 26 +- .../app/terminal/ITerm2TerminalType.java | 51 +++ .../xpipe/app/terminal/KittyTerminalType.java | 17 +- .../xpipe/app/terminal/MacOsTerminalType.java | 46 ++ .../app/terminal/MobaXTermTerminalType.java | 33 +- .../app/terminal/PowerShellTerminalType.java | 24 +- .../app/terminal/PtyxisTerminalType.java | 25 +- .../xpipe/app/terminal/PwshTerminalType.java | 45 +- .../app/terminal/SecureCrtTerminalType.java | 33 +- .../xpipe/app/terminal/TabbyTerminalType.java | 41 +- .../app/terminal/TrackableTerminalType.java | 2 +- .../xpipe/app/terminal/WarpTerminalType.java | 17 +- .../xpipe/app/terminal/WezTerminalType.java | 51 ++- .../app/terminal/XShellTerminalType.java | 23 +- .../io/xpipe/app/util/AppJacksonModule.java | 2 +- .../io/xpipe/app/util/LocalShellCache.java | 2 +- .../io/xpipe/app/vnc/ExternalVncClient.java | 46 ++ .../java/io/xpipe/app/vnc/TightVncClient.java | 47 ++ app/src/main/java/module-info.java | 3 +- 55 files changed, 1340 insertions(+), 829 deletions(-) delete mode 100644 app/src/main/java/io/xpipe/app/prefs/ExternalRdpClientType.java rename app/src/main/java/io/xpipe/app/{password => pwman}/BitwardenPasswordManager.java (99%) rename app/src/main/java/io/xpipe/app/{password => pwman}/DashlanePasswordManager.java (98%) rename app/src/main/java/io/xpipe/app/{password => pwman}/EnpassPasswordManager.java (96%) rename app/src/main/java/io/xpipe/app/{password => pwman}/KeePassXcAssociationKey.java (88%) rename app/src/main/java/io/xpipe/app/{password => pwman}/KeePassXcPasswordManager.java (99%) rename app/src/main/java/io/xpipe/app/{password => pwman}/KeePassXcProxyClient.java (99%) rename app/src/main/java/io/xpipe/app/{password => pwman}/KeeperPasswordManager.java (99%) rename app/src/main/java/io/xpipe/app/{password => pwman}/LastpassPasswordManager.java (99%) rename app/src/main/java/io/xpipe/app/{password => pwman}/OnePasswordManager.java (98%) rename app/src/main/java/io/xpipe/app/{password => pwman}/PasswordManager.java (97%) rename app/src/main/java/io/xpipe/app/{password => pwman}/PasswordManagerCommand.java (98%) rename app/src/main/java/io/xpipe/app/{password => pwman}/PasswordManagerCommandTemplate.java (98%) rename app/src/main/java/io/xpipe/app/{password => pwman}/PsonoPasswordManager.java (99%) rename app/src/main/java/io/xpipe/app/{password => pwman}/TweetNaClHelper.java (99%) rename app/src/main/java/io/xpipe/app/{password => pwman}/WindowsCredentialManager.java (99%) create mode 100644 app/src/main/java/io/xpipe/app/rdp/CustomRdpClient.java create mode 100644 app/src/main/java/io/xpipe/app/rdp/DevolutionsRdpClient.java create mode 100644 app/src/main/java/io/xpipe/app/rdp/ExternalRdpClient.java create mode 100644 app/src/main/java/io/xpipe/app/rdp/FreeRdpClient.java create mode 100644 app/src/main/java/io/xpipe/app/rdp/MstscRdpClient.java create mode 100644 app/src/main/java/io/xpipe/app/rdp/RdpLaunchConfig.java create mode 100644 app/src/main/java/io/xpipe/app/rdp/RemminaRdpClient.java create mode 100644 app/src/main/java/io/xpipe/app/rdp/RemoteDesktopAppRdpClient.java create mode 100644 app/src/main/java/io/xpipe/app/rdp/WindowsAppRdpClient.java create mode 100644 app/src/main/java/io/xpipe/app/terminal/ITerm2TerminalType.java create mode 100644 app/src/main/java/io/xpipe/app/terminal/MacOsTerminalType.java create mode 100644 app/src/main/java/io/xpipe/app/vnc/ExternalVncClient.java create mode 100644 app/src/main/java/io/xpipe/app/vnc/TightVncClient.java diff --git a/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java b/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java index 0f27195e7..d845ab701 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java @@ -15,7 +15,7 @@ import io.xpipe.app.ext.DataStoreProviders; import io.xpipe.app.ext.ProcessControlProvider; import io.xpipe.app.icon.SystemIconManager; import io.xpipe.app.issue.TrackEvent; -import io.xpipe.app.password.KeePassXcPasswordManager; +import io.xpipe.app.pwman.KeePassXcPasswordManager; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.resources.*; import io.xpipe.app.storage.DataStorage; diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java index 25b7af40d..c7316ee4e 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java @@ -1,14 +1,14 @@ package io.xpipe.app.prefs; -import io.xpipe.app.comp.Comp; import io.xpipe.app.core.*; import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.ext.PrefsHandler; import io.xpipe.app.ext.PrefsProvider; import io.xpipe.app.icon.SystemIconManager; import io.xpipe.app.icon.SystemIconSource; -import io.xpipe.app.password.PasswordManager; -import io.xpipe.app.password.PasswordManagerCommand; +import io.xpipe.app.pwman.PasswordManager; +import io.xpipe.app.pwman.PasswordManagerCommand; +import io.xpipe.app.rdp.ExternalRdpClient; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.terminal.ExternalTerminalType; import io.xpipe.app.terminal.TerminalMultiplexer; @@ -17,10 +17,8 @@ import io.xpipe.app.update.AppDistributionType; import io.xpipe.app.util.OptionsBuilder; import io.xpipe.app.util.PlatformState; import io.xpipe.app.util.PlatformThread; -import io.xpipe.app.util.SecretRetrievalStrategy; import io.xpipe.core.process.ShellScript; -import io.xpipe.core.util.SecretValue; import javafx.beans.property.*; import javafx.beans.value.ObservableBooleanValue; import javafx.beans.value.ObservableDoubleValue; @@ -74,8 +72,8 @@ public class AppPrefs { mapLocal(new SimpleBooleanProperty(true), "saveWindowLocation", Boolean.class, false); final ObjectProperty terminalType = mapLocal(new SimpleObjectProperty<>(), "terminalType", ExternalTerminalType.class, false); - final ObjectProperty rdpClientType = - mapLocal(new SimpleObjectProperty<>(), "rdpClientType", ExternalRdpClientType.class, false); + final ObjectProperty rdpClientType = + mapLocal(new SimpleObjectProperty<>(), "rdpClientType", ExternalRdpClient.class, false); final DoubleProperty windowOpacity = mapLocal(new SimpleDoubleProperty(1.0), "windowOpacity", Double.class, false); final StringProperty customRdpClientCommand = mapLocal(new SimpleStringProperty(null), "customRdpClientCommand", String.class, false); @@ -485,7 +483,7 @@ public class AppPrefs { return terminalType; } - public ObservableValue rdpClientType() { + public ObservableValue rdpClientType() { return rdpClientType; } @@ -558,7 +556,7 @@ public class AppPrefs { public void initDefaultValues() { externalEditor.setValue(ExternalEditorType.determineDefault(externalEditor.get())); terminalType.set(ExternalTerminalType.determineDefault(terminalType.get())); - rdpClientType.setValue(ExternalRdpClientType.determineDefault(rdpClientType.get())); + rdpClientType.setValue(ExternalRdpClient.determineDefault(rdpClientType.get())); if (AppProperties.get().isInitialLaunch()) { if (AppDistributionType.get() == AppDistributionType.WEBTOP) { performanceMode.setValue(true); diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java index 612932961..1e08a83af 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java @@ -13,37 +13,17 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Optional; -public abstract class ExternalApplicationType implements PrefsChoiceValue { - - private final String id; - - public ExternalApplicationType(String id) { - this.id = id; - } +public interface ExternalApplicationType extends PrefsChoiceValue { public abstract boolean isAvailable(); @Override - public String getId() { - return id; - } + public String getId(); - @Override - public String toString() { - return getId(); - } - - public abstract static class MacApplication extends ExternalApplicationType { - - protected final String applicationName; - - public MacApplication(String id, String applicationName) { - super(id); - this.applicationName = applicationName; - } + public interface MacApplication extends ExternalApplicationType { @Override - public boolean isAvailable() { + default boolean isAvailable() { try { return findApp().isPresent(); } catch (Exception e) { @@ -52,17 +32,19 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue { } } - public Optional findApp() throws Exception { + String getApplicationName(); + + default Optional findApp() throws Exception { // Perform a quick check because mdfind is slow - var applicationsDef = Path.of("/Applications/" + applicationName + ".app"); + var applicationsDef = Path.of("/Applications/" + getApplicationName() + ".app"); if (Files.exists(applicationsDef)) { return Optional.of(applicationsDef); } - var systemApplicationsDef = Path.of("/System/Applications/" + applicationName + ".app"); + var systemApplicationsDef = Path.of("/System/Applications/" + getApplicationName() + ".app"); if (Files.exists(systemApplicationsDef)) { return Optional.of(systemApplicationsDef); } - var userApplicationsDef = Path.of(System.getProperty("user.home") + "/Applications/" + applicationName + ".app"); + var userApplicationsDef = Path.of(System.getProperty("user.home") + "/Applications/" + getApplicationName() + ".app"); if (Files.exists(userApplicationsDef)) { return Optional.of(userApplicationsDef); } @@ -70,59 +52,54 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue { try (ShellControl pc = LocalShell.getShell().start()) { var out = pc.command(String.format( "mdfind -literal 'kMDItemFSName = \"%s.app\"' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications", - applicationName)) + getApplicationName())) .readStdoutIfPossible(); - return out.isPresent() && !out.get().isBlank() && out.get().contains(applicationName + ".app") + return out.isPresent() && !out.get().isBlank() && out.get().contains(getApplicationName() + ".app") ? out.map(s -> Path.of(s)) : Optional.empty(); } } - public void focus() { + default void focus() { try (ShellControl pc = LocalShell.getShell().start()) { - pc.command(String.format("open -a \"%s.app\"", applicationName)).execute(); + pc.command(String.format("open -a \"%s.app\"", getApplicationName())).execute(); } catch (Exception e) { ErrorEvent.fromThrowable(e).handle(); } } @Override - public boolean isSelectable() { + default boolean isSelectable() { return OsType.getLocal().equals(OsType.MACOS); } } - public abstract static class PathApplication extends ExternalApplicationType { + public interface PathApplication extends ExternalApplicationType { - protected final String executable; - protected final boolean explicitlyAsync; + String getExecutable(); - public PathApplication(String id, String executable, boolean explicitlyAsync) { - super(id); - this.executable = executable; - this.explicitlyAsync = explicitlyAsync; - } + boolean isExplicitlyAsync(); - public boolean isAvailable() { + default boolean isAvailable() { try (ShellControl pc = LocalShell.getShell()) { - return CommandSupport.findProgram(pc, executable).isPresent(); + return CommandSupport.findProgram(pc, getExecutable()).isPresent(); } catch (Exception e) { ErrorEvent.fromThrowable(e).omit().handle(); return false; } } - protected void launch(String title, CommandBuilder args) throws Exception { + default void launch(CommandBuilder args) throws Exception { try (ShellControl pc = LocalShell.getShell()) { - if (!CommandSupport.isInPath(pc, executable)) { + if (!CommandSupport.isInPath(pc, getExecutable())) { throw ErrorEvent.expected( new IOException( - "Executable " + executable + "Executable " + getExecutable() + " not found in PATH. Either add it to the PATH and refresh the environment by restarting XPipe, or specify an absolute executable path using the custom terminal setting.")); } - args.add(0, executable); - if (explicitlyAsync) { + args.add(0, getExecutable()); + if (isExplicitlyAsync()) { ExternalApplicationHelper.startAsync(args); } else { pc.executeSimpleCommand(args); @@ -131,21 +108,16 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue { } } - public abstract static class WindowsType extends ExternalApplicationType { + public interface WindowsType extends ExternalApplicationType { - private final String executable; + String getExecutable(); - public WindowsType(String id, String executable) { - super(id); - this.executable = executable; - } + public abstract Optional determineInstallation(); - protected abstract Optional determineInstallation(); - - protected Optional determineFromPath() { + default Optional determineFromPath() { // Try to locate if it is in the Path try (var sc = LocalShell.getShell().start()) { - var out = CommandSupport.findProgram(sc, executable); + var out = CommandSupport.findProgram(sc, getExecutable()); if (out.isPresent()) { return out.map(filePath -> Path.of(filePath.toString())); } @@ -155,8 +127,20 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue { return Optional.empty(); } + default Path findExecutable() { + var location = determineFromPath(); + if (location.isEmpty()) { + location = determineInstallation(); + if (location.isEmpty()) { + throw ErrorEvent.expected(new UnsupportedOperationException("Unable to find installation of " + + toTranslatedString().getValue())); + } + } + return location.get(); + } + @Override - public boolean isAvailable() { + default boolean isAvailable() { var path = determineFromPath(); if (path.isPresent() && Files.exists(path.get())) { return true; @@ -167,7 +151,7 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue { } @Override - public boolean isSelectable() { + default boolean isSelectable() { return OsType.getLocal().equals(OsType.WINDOWS); } } diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java index 99dd46f0c..6795bcf76 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java @@ -22,17 +22,48 @@ import java.util.function.Supplier; public interface ExternalEditorType extends PrefsChoiceValue { - ExternalEditorType NOTEPAD = new WindowsType("app.notepad", "notepad", false) { + ExternalEditorType NOTEPAD = new WindowsType() { + @Override - protected Optional determineInstallation() { + public String getId() { + return "app.notepad"; + } + + @Override + public boolean detach() { + return false; + } + + @Override + public String getExecutable() { + return "notepad"; + } + + @Override + public Optional determineInstallation() { return Optional.of(Path.of(System.getenv("SystemRoot") + "\\System32\\notepad.exe")); } }; - ExternalEditorType VSCODIUM_WINDOWS = new WindowsType("app.vscodium", "codium.cmd", false) { + ExternalEditorType VSCODIUM_WINDOWS = new WindowsType() { @Override - protected Optional determineInstallation() { + public String getId() { + return "app.vscodium"; + } + + @Override + public boolean detach() { + return false; + } + + @Override + public String getExecutable() { + return "codium.cmd"; + } + + @Override + public Optional determineInstallation() { return Optional.of(Path.of(System.getenv("LOCALAPPDATA")) .resolve("Programs") .resolve("VSCodium") @@ -42,10 +73,25 @@ public interface ExternalEditorType extends PrefsChoiceValue { } }; - WindowsType CURSOR_WINDOWS = new WindowsType("app.cursor", "Cursor", true) { + WindowsType CURSOR_WINDOWS = new WindowsType() { @Override - protected Optional determineInstallation() { + public String getId() { + return "app.cursor"; + } + + @Override + public boolean detach() { + return true; + } + + @Override + public String getExecutable() { + return "Cursor"; + } + + @Override + public Optional determineInstallation() { return Optional.of(Path.of(System.getenv("LOCALAPPDATA")) .resolve("Programs") .resolve("cursor") @@ -54,10 +100,25 @@ public interface ExternalEditorType extends PrefsChoiceValue { } }; - WindowsType VOID_WINDOWS = new WindowsType("app.void", "Void", true) { + WindowsType VOID_WINDOWS = new WindowsType() { @Override - protected Optional determineInstallation() { + public String getId() { + return "app.void"; + } + + @Override + public boolean detach() { + return true; + } + + @Override + public String getExecutable() { + return "Void"; + } + + @Override + public Optional determineInstallation() { return Optional.of(Path.of(System.getenv("PROGRAMFILES")) .resolve("Void") .resolve("Void.exe")) @@ -65,10 +126,25 @@ public interface ExternalEditorType extends PrefsChoiceValue { } }; - WindowsType WINDSURF_WINDOWS = new WindowsType("app.windsurf", "windsurf.cmd", false) { + WindowsType WINDSURF_WINDOWS = new WindowsType() { @Override - protected Optional determineInstallation() { + public String getId() { + return "app.windsurf"; + } + + @Override + public boolean detach() { + return false; + } + + @Override + public String getExecutable() { + return "windsurf.cmd"; + } + + @Override + public Optional determineInstallation() { return Optional.of(Path.of(System.getenv("LOCALAPPDATA")) .resolve("Programs") .resolve("Windsurf") @@ -79,10 +155,25 @@ public interface ExternalEditorType extends PrefsChoiceValue { }; // Cli is broken, keep inactive - WindowsType THEIAIDE_WINDOWS = new WindowsType("app.theiaide", "Theiaide", true) { + WindowsType THEIAIDE_WINDOWS = new WindowsType() { @Override - protected Optional determineInstallation() { + public String getId() { + return "app.theiaide"; + } + + @Override + public boolean detach() { + return true; + } + + @Override + public String getExecutable() { + return "Theiaide"; + } + + @Override + public Optional determineInstallation() { return Optional.of(Path.of(System.getenv("LOCALAPPDATA")) .resolve("Programs") .resolve("TheiaIDE") @@ -91,10 +182,25 @@ public interface ExternalEditorType extends PrefsChoiceValue { } }; - WindowsType TRAE_WINDOWS = new WindowsType("app.trae", "trae.cmd", false) { + WindowsType TRAE_WINDOWS = new WindowsType() { @Override - protected Optional determineInstallation() { + public String getId() { + return "app.trae"; + } + + @Override + public boolean detach() { + return false; + } + + @Override + public String getExecutable() { + return "trae.cmd"; + } + + @Override + public Optional determineInstallation() { return Optional.of(Path.of(System.getenv("LOCALAPPDATA")) .resolve("Programs") .resolve("Trae") @@ -104,10 +210,25 @@ public interface ExternalEditorType extends PrefsChoiceValue { } }; - WindowsType VSCODE_WINDOWS = new WindowsType("app.vscode", "code.cmd", false) { + WindowsType VSCODE_WINDOWS = new WindowsType() { @Override - protected Optional determineInstallation() { + public String getId() { + return "app.vscode"; + } + + @Override + public boolean detach() { + return false; + } + + @Override + public String getExecutable() { + return "code.cmd"; + } + + @Override + public Optional determineInstallation() { return Optional.of(Path.of(System.getenv("LOCALAPPDATA")) .resolve("Programs") .resolve("Microsoft VS Code") @@ -117,10 +238,25 @@ public interface ExternalEditorType extends PrefsChoiceValue { } }; - ExternalEditorType VSCODE_INSIDERS_WINDOWS = new WindowsType("app.vscodeInsiders", "code-insiders.cmd", false) { + ExternalEditorType VSCODE_INSIDERS_WINDOWS = new WindowsType() { @Override - protected Optional determineInstallation() { + public String getId() { + return "app.vscodeInsiders"; + } + + @Override + public boolean detach() { + return false; + } + + @Override + public String getExecutable() { + return "code-insiders.cmd"; + } + + @Override + public Optional determineInstallation() { return Optional.of(Path.of(System.getenv("LOCALAPPDATA")) .resolve("Programs") .resolve("Microsoft VS Code Insiders") @@ -130,10 +266,25 @@ public interface ExternalEditorType extends PrefsChoiceValue { } }; - ExternalEditorType NOTEPADPLUSPLUS = new WindowsType("app.notepad++", "notepad++", false) { + ExternalEditorType NOTEPADPLUSPLUS = new WindowsType() { @Override - protected Optional determineInstallation() { + public String getId() { + return "app.notepad++"; + } + + @Override + public boolean detach() { + return false; + } + + @Override + public String getExecutable() { + return "notepad++"; + } + + @Override + public Optional determineInstallation() { var found = WindowsRegistry.local() .readStringValueIfPresent(WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\Notepad++", null); @@ -152,7 +303,7 @@ public interface ExternalEditorType extends PrefsChoiceValue { public void launch(Path file) throws Exception { var builder = CommandBuilder.of() .fixedEnvironment("DONT_PROMPT_WSL_INSTALL", "No_Prompt_please") - .addFile(executable) + .addFile(getExecutable()) .addFile(file.toString()); ExternalApplicationHelper.startAsync(builder); } @@ -310,10 +461,14 @@ public interface ExternalEditorType extends PrefsChoiceValue { void launch(Path file) throws Exception; - class MacOsEditor extends ExternalApplicationType.MacApplication implements ExternalEditorType { + class MacOsEditor implements ExternalApplicationType.MacApplication, ExternalEditorType { - public MacOsEditor(String id, String applicationName) { - super(id, applicationName); + private final String id; + private final String appName; + + public MacOsEditor(String id, String appName) { + this.id = id; + this.appName = appName; } @Override @@ -321,33 +476,64 @@ public interface ExternalEditorType extends PrefsChoiceValue { try (var sc = LocalShell.getShell().start()) { sc.executeSimpleCommand(CommandBuilder.of() .add("open", "-a") - .addQuoted(applicationName) + .addQuoted(getApplicationName()) .addFile(file.toString())); } } + + @Override + public String getApplicationName() { + return appName; + } + + @Override + public String getId() { + return id; + } } - class GenericPathType extends ExternalApplicationType.PathApplication implements ExternalEditorType { + class GenericPathType implements ExternalApplicationType.PathApplication, ExternalEditorType { - public GenericPathType(String id, String command, boolean explicityAsync) { - super(id, command, explicityAsync); + private final String id; + private final String executable; + private final boolean async; + + public GenericPathType(String id, String executable, boolean async) { + this.id = id; + this.executable = executable; + this.async = async; } @Override public void launch(Path file) throws Exception { - var builder = CommandBuilder.of().addFile(executable).addFile(file.toString()); - if (explicitlyAsync) { + var builder = CommandBuilder.of().addFile(getExecutable()).addFile(file.toString()); + if (isExplicitlyAsync()) { ExternalApplicationHelper.startAsync(builder); } else { LocalShell.getShell().executeSimpleCommand(builder); } } + + @Override + public String getExecutable() { + return executable; + } + + @Override + public boolean isExplicitlyAsync() { + return async; + } + + @Override + public String getId() { + return id; + } } class LinuxPathType extends GenericPathType { - public LinuxPathType(String id, String command) { - super(id, command, true); + public LinuxPathType(String id, String executable) { + super(id, executable, true); } @Override @@ -356,37 +542,19 @@ public interface ExternalEditorType extends PrefsChoiceValue { } } - abstract class WindowsType extends ExternalApplicationType.WindowsType implements ExternalEditorType { + interface WindowsType extends ExternalApplicationType.WindowsType, ExternalEditorType { - private final boolean detach; - - public WindowsType(String id, String executable, boolean detach) { - super(id, executable); - this.detach = detach; - } + boolean detach(); @Override - public void launch(Path file) throws Exception { + default void launch(Path file) throws Exception { var location = findExecutable(); - if (location.isEmpty()) { - throw ErrorEvent.expected(new IOException( - "Unable to find installation of " + toTranslatedString().getValue())); - } - - var builder = CommandBuilder.of().addFile(location.get().toString()).addFile(file.toString()); - if (detach) { + var builder = CommandBuilder.of().addFile(location.toString()).addFile(file.toString()); + if (detach()) { ExternalApplicationHelper.startAsync(builder); } else { LocalShell.getShell().executeSimpleCommand(builder); } } - - public Optional findExecutable() { - var location = determineFromPath(); - if (location.isEmpty()) { - location = determineInstallation(); - } - return location; - } } } diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalRdpClientType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalRdpClientType.java deleted file mode 100644 index c17da23ae..000000000 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalRdpClientType.java +++ /dev/null @@ -1,412 +0,0 @@ -package io.xpipe.app.prefs; - -import io.xpipe.app.ext.PrefsChoiceValue; -import io.xpipe.app.issue.ErrorEvent; -import io.xpipe.app.util.*; -import io.xpipe.core.process.CommandBuilder; -import io.xpipe.core.process.OsType; -import io.xpipe.core.util.SecretValue; - -import lombok.Value; -import org.apache.commons.io.FileUtils; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; -import java.util.function.Supplier; -import javax.crypto.Cipher; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; - -public interface ExternalRdpClientType extends PrefsChoiceValue { - - static ExternalRdpClientType getApplicationLauncher() { - if (OsType.getLocal() == OsType.WINDOWS) { - return MSTSC; - } else { - return AppPrefs.get().rdpClientType().getValue(); - } - } - - ExternalRdpClientType MSTSC = new PathCheckType("app.mstsc", "mstsc.exe", false) { - - @Override - public void launch(LaunchConfiguration configuration) throws Exception { - var adaptedRdpConfig = getAdaptedConfig(configuration); - var file = writeRdpConfigFile(configuration.getTitle(), adaptedRdpConfig); - LocalShell.getShell() - .executeSimpleCommand(CommandBuilder.of().add(executable).addFile(file.toString())); - ThreadHelper.runFailableAsync(() -> { - ThreadHelper.sleep(1000); - FileUtils.deleteQuietly(file.toFile()); - }); - } - - @Override - public boolean supportsPasswordPassing() { - return true; - } - - private RdpConfig getAdaptedConfig(LaunchConfiguration configuration) throws Exception { - var input = configuration.getConfig(); - if (input.get("password 51").isPresent()) { - return input; - } - - if (input.get("username").isEmpty()) { - // return input; - } - - var pass = configuration.getPassword(); - if (pass == null) { - return input; - } - - var adapted = input.overlay(Map.of( - "password 51", - new RdpConfig.TypedValue("b", encrypt(pass)), - "prompt for credentials", - new RdpConfig.TypedValue("i", "0"))); - return adapted; - } - - private String encrypt(SecretValue password) throws Exception { - var ps = LocalShell.getLocalPowershell(); - var cmd = ps.command(CommandBuilder.of().add(sc -> "(" + sc.getShellDialect().literalArgument(password.getSecretValue()) + " | ConvertTo-SecureString -AsPlainText -Force) | ConvertFrom-SecureString")); - cmd.sensitive(); - return cmd.readStdoutOrThrow(); - } - }; - - ExternalRdpClientType DEVOLUTIONS = new WindowsType("app.devolutions", "RemoteDesktopManager") { - - @Override - protected Optional determineInstallation() { - try { - var r = WindowsRegistry.local() - .readStringValueIfPresent( - WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\Classes\\rdm\\DefaultIcon"); - return r.map(Path::of); - } catch (Exception e) { - ErrorEvent.fromThrowable(e).omit().handle(); - return Optional.empty(); - } - } - - @Override - protected void execute(Path file, LaunchConfiguration configuration) throws Exception { - var config = writeRdpConfigFile(configuration.getTitle(), configuration.getConfig()); - LocalShell.getShell() - .executeSimpleCommand(CommandBuilder.of() - .addFile(file.toString()) - .addFile(config.toString()) - .discardAllOutput()); - ThreadHelper.runFailableAsync(() -> { - // Startup is slow - ThreadHelper.sleep(10000); - FileUtils.deleteQuietly(config.toFile()); - }); - } - - @Override - public boolean supportsPasswordPassing() { - return false; - } - }; - - ExternalRdpClientType REMMINA = new RemminaRdpType(); - - ExternalRdpClientType X_FREE_RDP = new PathCheckType("app.xfreeRdp", "xfreerdp", true) { - - @Override - public void launch(LaunchConfiguration configuration) throws Exception { - var file = writeRdpConfigFile(configuration.getTitle(), configuration.getConfig()); - var b = CommandBuilder.of().addFile(file.toString()).add("/cert-ignore"); - if (configuration.getPassword() != null) { - var escapedPw = configuration.getPassword().getSecretValue().replaceAll("'", "\\\\'"); - b.add("/p:'" + escapedPw + "'"); - } - launch(configuration.getTitle(), b); - } - - @Override - public boolean supportsPasswordPassing() { - return true; - } - }; - - ExternalRdpClientType MICROSOFT_REMOTE_DESKTOP_MACOS_APP = - new MacOsType("app.microsoftRemoteDesktopApp", "Microsoft Remote Desktop") { - - @Override - public void launch(LaunchConfiguration configuration) throws Exception { - var file = writeRdpConfigFile(configuration.getTitle(), configuration.getConfig()); - LocalShell.getShell() - .executeSimpleCommand(CommandBuilder.of() - .add("open", "-a") - .addQuoted("Microsoft Remote Desktop.app") - .addFile(file.toString())); - } - - @Override - public boolean supportsPasswordPassing() { - return false; - } - }; - - ExternalRdpClientType WINDOWS_APP_MACOS = new MacOsType("app.windowsApp", "Windows App") { - - @Override - public void launch(LaunchConfiguration configuration) throws Exception { - var file = writeRdpConfigFile(configuration.getTitle(), configuration.getConfig()); - LocalShell.getShell() - .executeSimpleCommand(CommandBuilder.of() - .add("open", "-a") - .addQuoted("Windows App.app") - .addFile(file.toString())); - } - - @Override - public boolean supportsPasswordPassing() { - return false; - } - }; - - ExternalRdpClientType CUSTOM = new CustomType(); - List WINDOWS_CLIENTS = List.of(MSTSC, DEVOLUTIONS); - List LINUX_CLIENTS = List.of(REMMINA, X_FREE_RDP); - List MACOS_CLIENTS = List.of(MICROSOFT_REMOTE_DESKTOP_MACOS_APP, WINDOWS_APP_MACOS); - - @SuppressWarnings("TrivialFunctionalExpressionUsage") - List ALL = ((Supplier>) () -> { - var all = new ArrayList(); - if (OsType.getLocal().equals(OsType.WINDOWS)) { - all.addAll(WINDOWS_CLIENTS); - } - if (OsType.getLocal().equals(OsType.LINUX)) { - all.addAll(LINUX_CLIENTS); - } - if (OsType.getLocal().equals(OsType.MACOS)) { - all.addAll(MACOS_CLIENTS); - } - all.add(CUSTOM); - return all; - }) - .get(); - - static ExternalRdpClientType determineDefault(ExternalRdpClientType existing) { - // Verify that our selection is still valid - if (existing != null && existing.isAvailable()) { - return existing; - } - - var r = ALL.stream() - .filter(t -> !t.equals(CUSTOM)) - .filter(t -> t.isAvailable()) - .findFirst() - .orElse(null); - - // Check if detection failed for some reason - if (r == null) { - var def = OsType.getLocal() == OsType.WINDOWS - ? MSTSC - : OsType.getLocal() == OsType.MACOS ? WINDOWS_APP_MACOS : REMMINA; - r = def; - } - - return r; - } - - void launch(LaunchConfiguration configuration) throws Exception; - - boolean supportsPasswordPassing(); - - default Path writeRdpConfigFile(String title, RdpConfig input) throws Exception { - var name = OsType.getLocal().makeFileSystemCompatible(title); - var file = ShellTemp.getLocalTempDataDirectory("rdp").resolve(name + ".rdp"); - var string = input.toString(); - Files.createDirectories(file.getParent()); - Files.writeString(file, string); - return file; - } - - @Value - class LaunchConfiguration { - String title; - RdpConfig config; - UUID storeId; - SecretValue password; - } - - abstract class WindowsType extends ExternalApplicationType.WindowsType implements ExternalRdpClientType { - - public WindowsType(String id, String executable) { - super(id, executable); - } - - @Override - public void launch(LaunchConfiguration configuration) throws Exception { - var location = determineFromPath(); - if (location.isEmpty()) { - location = determineInstallation(); - if (location.isEmpty()) { - throw ErrorEvent.expected(new IOException("Unable to find installation of " - + toTranslatedString().getValue())); - } - } - - execute(location.get(), configuration); - } - - protected abstract void execute(Path file, LaunchConfiguration configuration) throws Exception; - } - - abstract class PathCheckType extends ExternalApplicationType.PathApplication implements ExternalRdpClientType { - - public PathCheckType(String id, String executable, boolean explicityAsync) { - super(id, executable, explicityAsync); - } - } - - abstract class MacOsType extends ExternalApplicationType.MacApplication implements ExternalRdpClientType { - - public MacOsType(String id, String applicationName) { - super(id, applicationName); - } - } - - class CustomType extends ExternalApplicationType implements ExternalRdpClientType { - - public CustomType() { - super("app.custom"); - } - - @Override - public void launch(LaunchConfiguration configuration) throws Exception { - var customCommand = AppPrefs.get().customRdpClientCommand().getValue(); - if (customCommand == null || customCommand.isBlank()) { - throw ErrorEvent.expected(new IllegalStateException("No custom RDP command specified")); - } - - var format = - customCommand.toLowerCase(Locale.ROOT).contains("$file") ? customCommand : customCommand + " $FILE"; - ExternalApplicationHelper.startAsync(CommandBuilder.of() - .add(ExternalApplicationHelper.replaceVariableArgument( - format, - "FILE", - writeRdpConfigFile(configuration.getTitle(), configuration.getConfig()) - .toString()))); - } - - @Override - public boolean supportsPasswordPassing() { - return false; - } - - @Override - public boolean isAvailable() { - return true; - } - } - - class RemminaRdpType extends ExternalApplicationType.PathApplication implements ExternalRdpClientType { - - public RemminaRdpType() { - super("app.remmina", "remmina", true); - } - - private List toStrip() { - return List.of("auto connect", "password 51", "prompt for credentials", "smart sizing"); - } - - @Override - public void launch(LaunchConfiguration configuration) throws Exception { - RdpConfig c = configuration.getConfig(); - var l = new HashSet<>(c.getContent().keySet()); - toStrip().forEach(l::remove); - if (l.size() == 2 && l.contains("username") && l.contains("full address")) { - var encrypted = encryptPassword(configuration.getPassword()); - if (encrypted.isPresent()) { - var file = writeRemminaConfigFile(configuration, encrypted.get()); - launch( - configuration.getTitle(), - CommandBuilder.of().add("-c").addFile(file.toString())); - ThreadHelper.runFailableAsync(() -> { - ThreadHelper.sleep(5000); - FileUtils.deleteQuietly(file.toFile()); - }); - return; - } - } - - var file = writeRdpConfigFile(configuration.getTitle(), c); - launch(configuration.getTitle(), CommandBuilder.of().add("-c").addFile(file.toString())); - } - - private Optional encryptPassword(SecretValue password) throws Exception { - if (password == null) { - return Optional.empty(); - } - - try (var sc = LocalShell.getShell().start()) { - var prefSecretBase64 = sc.command("sed -n 's/^secret=//p' ~/.config/remmina/remmina.pref") - .readStdoutIfPossible(); - if (prefSecretBase64.isEmpty()) { - return Optional.empty(); - } - - var paddedPassword = password.getSecretValue(); - paddedPassword = paddedPassword + "\0".repeat(8 - paddedPassword.length() % 8); - var prefSecret = Base64.getDecoder().decode(prefSecretBase64.get()); - var key = Arrays.copyOfRange(prefSecret, 0, 24); - var iv = Arrays.copyOfRange(prefSecret, 24, prefSecret.length); - - var cipher = Cipher.getInstance("DESede/CBC/Nopadding"); - var keySpec = new SecretKeySpec(key, "DESede"); - var ivspec = new IvParameterSpec(iv); - cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivspec); - byte[] encryptedText = cipher.doFinal(paddedPassword.getBytes(StandardCharsets.UTF_8)); - var base64Encrypted = Base64.getEncoder().encodeToString(encryptedText); - return Optional.ofNullable(base64Encrypted); - } - } - - private Path writeRemminaConfigFile(LaunchConfiguration configuration, String password) throws Exception { - var name = OsType.getLocal().makeFileSystemCompatible(configuration.getTitle()); - var file = ShellTemp.getLocalTempDataDirectory("rdp").resolve(name + ".remmina"); - var string = - """ - [remmina] - protocol=RDP - name=%s - username=%s - server=%s - password=%s - cert_ignore=1 - """ - .formatted( - configuration.getTitle(), - configuration - .getConfig() - .get("username") - .orElseThrow() - .getValue(), - configuration - .getConfig() - .get("full address") - .orElseThrow() - .getValue(), - password); - Files.createDirectories(file.getParent()); - Files.writeString(file, string); - return file; - } - - @Override - public boolean supportsPasswordPassing() { - return false; - } - } -} diff --git a/app/src/main/java/io/xpipe/app/prefs/PasswordManagerCategory.java b/app/src/main/java/io/xpipe/app/prefs/PasswordManagerCategory.java index 52f9c15c3..0fb70a049 100644 --- a/app/src/main/java/io/xpipe/app/prefs/PasswordManagerCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/PasswordManagerCategory.java @@ -6,7 +6,7 @@ import io.xpipe.app.comp.base.HorizontalComp; import io.xpipe.app.comp.base.LabelComp; import io.xpipe.app.comp.base.TextFieldComp; import io.xpipe.app.core.AppI18n; -import io.xpipe.app.password.PasswordManager; +import io.xpipe.app.pwman.PasswordManager; import io.xpipe.app.util.*; import javafx.application.Platform; diff --git a/app/src/main/java/io/xpipe/app/prefs/RdpCategory.java b/app/src/main/java/io/xpipe/app/prefs/RdpCategory.java index 061fb2bac..5166896ef 100644 --- a/app/src/main/java/io/xpipe/app/prefs/RdpCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/RdpCategory.java @@ -4,6 +4,7 @@ import io.xpipe.app.comp.Comp; import io.xpipe.app.comp.base.ChoiceComp; import io.xpipe.app.comp.base.TextFieldComp; import io.xpipe.app.ext.PrefsChoiceValue; +import io.xpipe.app.rdp.ExternalRdpClient; import io.xpipe.app.util.OptionsBuilder; public class RdpCategory extends AppPrefsCategory { @@ -21,11 +22,11 @@ public class RdpCategory extends AppPrefsCategory { .sub(new OptionsBuilder() .nameAndDescription("rdpClient") .addComp(ChoiceComp.ofTranslatable( - prefs.rdpClientType, PrefsChoiceValue.getSupported(ExternalRdpClientType.class), false)) + prefs.rdpClientType, PrefsChoiceValue.getSupported(ExternalRdpClient.class), false)) .nameAndDescription("customRdpClientCommand") .addComp(new TextFieldComp(prefs.customRdpClientCommand, true) .apply(struc -> struc.get().setPromptText("myrdpclient -c $FILE")) - .hide(prefs.rdpClientType.isNotEqualTo(ExternalRdpClientType.CUSTOM)))) + .hide(prefs.rdpClientType.isNotEqualTo(ExternalRdpClient.CUSTOM)))) .buildComp(); } } diff --git a/app/src/main/java/io/xpipe/app/password/BitwardenPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/BitwardenPasswordManager.java similarity index 99% rename from app/src/main/java/io/xpipe/app/password/BitwardenPasswordManager.java rename to app/src/main/java/io/xpipe/app/pwman/BitwardenPasswordManager.java index 3cf015798..bd00a5588 100644 --- a/app/src/main/java/io/xpipe/app/password/BitwardenPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/BitwardenPasswordManager.java @@ -1,4 +1,4 @@ -package io.xpipe.app.password; +package io.xpipe.app.pwman; import io.xpipe.app.ext.ProcessControlProvider; import io.xpipe.app.issue.ErrorEvent; diff --git a/app/src/main/java/io/xpipe/app/password/DashlanePasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/DashlanePasswordManager.java similarity index 98% rename from app/src/main/java/io/xpipe/app/password/DashlanePasswordManager.java rename to app/src/main/java/io/xpipe/app/pwman/DashlanePasswordManager.java index a0890f453..387fe007e 100644 --- a/app/src/main/java/io/xpipe/app/password/DashlanePasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/DashlanePasswordManager.java @@ -1,4 +1,4 @@ -package io.xpipe.app.password; +package io.xpipe.app.pwman; import io.xpipe.app.ext.ProcessControlProvider; import io.xpipe.app.issue.ErrorEvent; diff --git a/app/src/main/java/io/xpipe/app/password/EnpassPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/EnpassPasswordManager.java similarity index 96% rename from app/src/main/java/io/xpipe/app/password/EnpassPasswordManager.java rename to app/src/main/java/io/xpipe/app/pwman/EnpassPasswordManager.java index aa2818beb..bc5df4279 100644 --- a/app/src/main/java/io/xpipe/app/password/EnpassPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/EnpassPasswordManager.java @@ -1,17 +1,13 @@ -package io.xpipe.app.password; +package io.xpipe.app.pwman; import com.fasterxml.jackson.annotation.JsonTypeName; -import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.comp.base.ContextualFileReferenceChoiceComp; -import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.ProcessControlProvider; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.storage.DataStorage; -import io.xpipe.app.terminal.TerminalLauncher; import io.xpipe.app.util.*; import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.ShellControl; -import io.xpipe.core.process.ShellScript; import io.xpipe.core.store.FilePath; import io.xpipe.core.util.InPlaceSecretValue; import io.xpipe.core.util.JacksonMapper; diff --git a/app/src/main/java/io/xpipe/app/password/KeePassXcAssociationKey.java b/app/src/main/java/io/xpipe/app/pwman/KeePassXcAssociationKey.java similarity index 88% rename from app/src/main/java/io/xpipe/app/password/KeePassXcAssociationKey.java rename to app/src/main/java/io/xpipe/app/pwman/KeePassXcAssociationKey.java index 220a807f3..8da549b6b 100644 --- a/app/src/main/java/io/xpipe/app/password/KeePassXcAssociationKey.java +++ b/app/src/main/java/io/xpipe/app/pwman/KeePassXcAssociationKey.java @@ -1,4 +1,4 @@ -package io.xpipe.app.password; +package io.xpipe.app.pwman; import io.xpipe.core.util.InPlaceSecretValue; diff --git a/app/src/main/java/io/xpipe/app/password/KeePassXcPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/KeePassXcPasswordManager.java similarity index 99% rename from app/src/main/java/io/xpipe/app/password/KeePassXcPasswordManager.java rename to app/src/main/java/io/xpipe/app/pwman/KeePassXcPasswordManager.java index 3a9d3aea1..e0a04cac7 100644 --- a/app/src/main/java/io/xpipe/app/password/KeePassXcPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/KeePassXcPasswordManager.java @@ -1,4 +1,4 @@ -package io.xpipe.app.password; +package io.xpipe.app.pwman; import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.core.AppI18n; diff --git a/app/src/main/java/io/xpipe/app/password/KeePassXcProxyClient.java b/app/src/main/java/io/xpipe/app/pwman/KeePassXcProxyClient.java similarity index 99% rename from app/src/main/java/io/xpipe/app/password/KeePassXcProxyClient.java rename to app/src/main/java/io/xpipe/app/pwman/KeePassXcProxyClient.java index 6a74348fa..71cc8cfbd 100644 --- a/app/src/main/java/io/xpipe/app/password/KeePassXcProxyClient.java +++ b/app/src/main/java/io/xpipe/app/pwman/KeePassXcProxyClient.java @@ -1,4 +1,4 @@ -package io.xpipe.app.password; +package io.xpipe.app.pwman; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.util.DocumentationLink; diff --git a/app/src/main/java/io/xpipe/app/password/KeeperPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java similarity index 99% rename from app/src/main/java/io/xpipe/app/password/KeeperPasswordManager.java rename to app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java index bb8d3d19d..fae348b9e 100644 --- a/app/src/main/java/io/xpipe/app/password/KeeperPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java @@ -1,4 +1,4 @@ -package io.xpipe.app.password; +package io.xpipe.app.pwman; import com.fasterxml.jackson.databind.JsonNode; import io.xpipe.app.ext.ProcessControlProvider; diff --git a/app/src/main/java/io/xpipe/app/password/LastpassPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/LastpassPasswordManager.java similarity index 99% rename from app/src/main/java/io/xpipe/app/password/LastpassPasswordManager.java rename to app/src/main/java/io/xpipe/app/pwman/LastpassPasswordManager.java index 77786c14d..55ee593c8 100644 --- a/app/src/main/java/io/xpipe/app/password/LastpassPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/LastpassPasswordManager.java @@ -1,4 +1,4 @@ -package io.xpipe.app.password; +package io.xpipe.app.pwman; import io.xpipe.app.ext.ProcessControlProvider; import io.xpipe.app.issue.ErrorEvent; diff --git a/app/src/main/java/io/xpipe/app/password/OnePasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/OnePasswordManager.java similarity index 98% rename from app/src/main/java/io/xpipe/app/password/OnePasswordManager.java rename to app/src/main/java/io/xpipe/app/pwman/OnePasswordManager.java index be1385512..147656e6b 100644 --- a/app/src/main/java/io/xpipe/app/password/OnePasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/OnePasswordManager.java @@ -1,4 +1,4 @@ -package io.xpipe.app.password; +package io.xpipe.app.pwman; import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.ProcessControlProvider; diff --git a/app/src/main/java/io/xpipe/app/password/PasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/PasswordManager.java similarity index 97% rename from app/src/main/java/io/xpipe/app/password/PasswordManager.java rename to app/src/main/java/io/xpipe/app/pwman/PasswordManager.java index 6b8808fee..76a72ec54 100644 --- a/app/src/main/java/io/xpipe/app/password/PasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/PasswordManager.java @@ -1,4 +1,4 @@ -package io.xpipe.app.password; +package io.xpipe.app.pwman; import io.xpipe.core.process.OsType; import io.xpipe.core.util.SecretValue; diff --git a/app/src/main/java/io/xpipe/app/password/PasswordManagerCommand.java b/app/src/main/java/io/xpipe/app/pwman/PasswordManagerCommand.java similarity index 98% rename from app/src/main/java/io/xpipe/app/password/PasswordManagerCommand.java rename to app/src/main/java/io/xpipe/app/pwman/PasswordManagerCommand.java index 3c09967e0..42d39e10b 100644 --- a/app/src/main/java/io/xpipe/app/password/PasswordManagerCommand.java +++ b/app/src/main/java/io/xpipe/app/pwman/PasswordManagerCommand.java @@ -1,4 +1,4 @@ -package io.xpipe.app.password; +package io.xpipe.app.pwman; import io.xpipe.app.comp.Comp; import io.xpipe.app.comp.base.IntegratedTextAreaComp; @@ -13,7 +13,6 @@ import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellScript; import io.xpipe.core.util.InPlaceSecretValue; import io.xpipe.core.util.SecretValue; -import io.xpipe.core.util.ValidationException; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; diff --git a/app/src/main/java/io/xpipe/app/password/PasswordManagerCommandTemplate.java b/app/src/main/java/io/xpipe/app/pwman/PasswordManagerCommandTemplate.java similarity index 98% rename from app/src/main/java/io/xpipe/app/password/PasswordManagerCommandTemplate.java rename to app/src/main/java/io/xpipe/app/pwman/PasswordManagerCommandTemplate.java index 5ea2b90b4..19aae89d7 100644 --- a/app/src/main/java/io/xpipe/app/password/PasswordManagerCommandTemplate.java +++ b/app/src/main/java/io/xpipe/app/pwman/PasswordManagerCommandTemplate.java @@ -1,4 +1,4 @@ -package io.xpipe.app.password; +package io.xpipe.app.pwman; import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.core.process.OsType; diff --git a/app/src/main/java/io/xpipe/app/password/PsonoPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/PsonoPasswordManager.java similarity index 99% rename from app/src/main/java/io/xpipe/app/password/PsonoPasswordManager.java rename to app/src/main/java/io/xpipe/app/pwman/PsonoPasswordManager.java index 8e9f7f023..fdd737e5a 100644 --- a/app/src/main/java/io/xpipe/app/password/PsonoPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/PsonoPasswordManager.java @@ -1,4 +1,4 @@ -package io.xpipe.app.password; +package io.xpipe.app.pwman; import com.fasterxml.jackson.annotation.JsonTypeName; import io.xpipe.app.comp.base.SecretFieldComp; diff --git a/app/src/main/java/io/xpipe/app/password/TweetNaClHelper.java b/app/src/main/java/io/xpipe/app/pwman/TweetNaClHelper.java similarity index 99% rename from app/src/main/java/io/xpipe/app/password/TweetNaClHelper.java rename to app/src/main/java/io/xpipe/app/pwman/TweetNaClHelper.java index 8610dd91a..26dbb47ca 100644 --- a/app/src/main/java/io/xpipe/app/password/TweetNaClHelper.java +++ b/app/src/main/java/io/xpipe/app/pwman/TweetNaClHelper.java @@ -1,4 +1,4 @@ -package io.xpipe.app.password; +package io.xpipe.app.pwman; import org.bouncycastle.crypto.AsymmetricCipherKeyPair; import org.bouncycastle.crypto.KeyGenerationParameters; diff --git a/app/src/main/java/io/xpipe/app/password/WindowsCredentialManager.java b/app/src/main/java/io/xpipe/app/pwman/WindowsCredentialManager.java similarity index 99% rename from app/src/main/java/io/xpipe/app/password/WindowsCredentialManager.java rename to app/src/main/java/io/xpipe/app/pwman/WindowsCredentialManager.java index bf978695b..55b002677 100644 --- a/app/src/main/java/io/xpipe/app/password/WindowsCredentialManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/WindowsCredentialManager.java @@ -1,4 +1,4 @@ -package io.xpipe.app.password; +package io.xpipe.app.pwman; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.util.LocalShell; diff --git a/app/src/main/java/io/xpipe/app/rdp/CustomRdpClient.java b/app/src/main/java/io/xpipe/app/rdp/CustomRdpClient.java new file mode 100644 index 000000000..9980e2f6e --- /dev/null +++ b/app/src/main/java/io/xpipe/app/rdp/CustomRdpClient.java @@ -0,0 +1,40 @@ +package io.xpipe.app.rdp; + +import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.prefs.ExternalApplicationHelper; +import io.xpipe.app.prefs.ExternalApplicationType; +import io.xpipe.core.process.CommandBuilder; + +import java.util.Locale; + +public class CustomRdpClient implements ExternalApplicationType, ExternalRdpClient { + + @Override + public void launch(RdpLaunchConfig configuration) throws Exception { + var customCommand = AppPrefs.get().customRdpClientCommand().getValue(); + if (customCommand == null || customCommand.isBlank()) { + throw ErrorEvent.expected(new IllegalStateException("No custom RDP command specified")); + } + + var format = customCommand.toLowerCase(Locale.ROOT).contains("$file") ? customCommand : customCommand + " $FILE"; + ExternalApplicationHelper.startAsync(CommandBuilder.of() + .add(ExternalApplicationHelper.replaceVariableArgument(format, "FILE", + writeRdpConfigFile(configuration.getTitle(), configuration.getConfig()).toString()))); + } + + @Override + public boolean supportsPasswordPassing() { + return false; + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public String getId() { + return "app.custom"; + } +} diff --git a/app/src/main/java/io/xpipe/app/rdp/DevolutionsRdpClient.java b/app/src/main/java/io/xpipe/app/rdp/DevolutionsRdpClient.java new file mode 100644 index 000000000..6b105afc8 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/rdp/DevolutionsRdpClient.java @@ -0,0 +1,53 @@ +package io.xpipe.app.rdp; + +import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.prefs.ExternalApplicationType; +import io.xpipe.app.util.LocalShell; +import io.xpipe.app.util.ThreadHelper; +import io.xpipe.app.util.WindowsRegistry; +import io.xpipe.core.process.CommandBuilder; +import org.apache.commons.io.FileUtils; + +import java.nio.file.Path; +import java.util.Optional; + +public class DevolutionsRdpClient implements ExternalApplicationType.WindowsType, ExternalRdpClient { + + @Override + public String getExecutable() { + return "RemoteDesktopManager"; + } + + @Override + public Optional determineInstallation() { + try { + var r = WindowsRegistry.local().readStringValueIfPresent(WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\Classes\\rdm\\DefaultIcon"); + return r.map(Path::of); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).omit().handle(); + return Optional.empty(); + } + } + + @Override + public void launch(RdpLaunchConfig configuration) throws Exception { + var location = findExecutable(); + var config = writeRdpConfigFile(configuration.getTitle(), configuration.getConfig()); + LocalShell.getShell().executeSimpleCommand(CommandBuilder.of().addFile(location).addFile(config).discardAllOutput()); + ThreadHelper.runFailableAsync(() -> { + // Startup is slow + ThreadHelper.sleep(10000); + FileUtils.deleteQuietly(config.toFile()); + }); + } + + @Override + public boolean supportsPasswordPassing() { + return false; + } + + @Override + public String getId() { + return "app.devolutions"; + } +} diff --git a/app/src/main/java/io/xpipe/app/rdp/ExternalRdpClient.java b/app/src/main/java/io/xpipe/app/rdp/ExternalRdpClient.java new file mode 100644 index 000000000..17790c0a6 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/rdp/ExternalRdpClient.java @@ -0,0 +1,93 @@ +package io.xpipe.app.rdp; + +import io.xpipe.app.ext.PrefsChoiceValue; +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.util.*; +import io.xpipe.core.process.OsType; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.function.Supplier; + +public interface ExternalRdpClient extends PrefsChoiceValue { + + static ExternalRdpClient getApplicationLauncher() { + if (OsType.getLocal() == OsType.WINDOWS) { + return MSTSC; + } else { + return AppPrefs.get().rdpClientType().getValue(); + } + } + + ExternalRdpClient MSTSC = new MstscRdpClient(); + + ExternalRdpClient DEVOLUTIONS = new DevolutionsRdpClient(); + + ExternalRdpClient REMMINA = new RemminaRdpClient(); + + ExternalRdpClient X_FREE_RDP = new FreeRdpClient(); + + ExternalRdpClient MICROSOFT_REMOTE_DESKTOP_MACOS_APP = new RemoteDesktopAppRdpClient(); + + ExternalRdpClient WINDOWS_APP_MACOS = new WindowsAppRdpClient(); + + ExternalRdpClient CUSTOM = new CustomRdpClient(); + List WINDOWS_CLIENTS = List.of(MSTSC, DEVOLUTIONS); + List LINUX_CLIENTS = List.of(REMMINA, X_FREE_RDP); + List MACOS_CLIENTS = List.of(MICROSOFT_REMOTE_DESKTOP_MACOS_APP, WINDOWS_APP_MACOS); + + @SuppressWarnings("TrivialFunctionalExpressionUsage") + List ALL = ((Supplier>) () -> { + var all = new ArrayList(); + if (OsType.getLocal().equals(OsType.WINDOWS)) { + all.addAll(WINDOWS_CLIENTS); + } + if (OsType.getLocal().equals(OsType.LINUX)) { + all.addAll(LINUX_CLIENTS); + } + if (OsType.getLocal().equals(OsType.MACOS)) { + all.addAll(MACOS_CLIENTS); + } + all.add(CUSTOM); + return all; + }) + .get(); + + static ExternalRdpClient determineDefault(ExternalRdpClient existing) { + // Verify that our selection is still valid + if (existing != null && existing.isAvailable()) { + return existing; + } + + var r = ALL.stream() + .filter(t -> !t.equals(CUSTOM)) + .filter(t -> t.isAvailable()) + .findFirst() + .orElse(null); + + // Check if detection failed for some reason + if (r == null) { + var def = OsType.getLocal() == OsType.WINDOWS + ? MSTSC + : OsType.getLocal() == OsType.MACOS ? WINDOWS_APP_MACOS : REMMINA; + r = def; + } + + return r; + } + + void launch(RdpLaunchConfig configuration) throws Exception; + + boolean supportsPasswordPassing(); + + default Path writeRdpConfigFile(String title, RdpConfig input) throws Exception { + var name = OsType.getLocal().makeFileSystemCompatible(title); + var file = ShellTemp.getLocalTempDataDirectory("rdp").resolve(name + ".rdp"); + var string = input.toString(); + Files.createDirectories(file.getParent()); + Files.writeString(file, string); + return file; + } + +} diff --git a/app/src/main/java/io/xpipe/app/rdp/FreeRdpClient.java b/app/src/main/java/io/xpipe/app/rdp/FreeRdpClient.java new file mode 100644 index 000000000..5a36117f4 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/rdp/FreeRdpClient.java @@ -0,0 +1,38 @@ +package io.xpipe.app.rdp; + +import io.xpipe.app.prefs.ExternalApplicationType; +import io.xpipe.core.process.CommandBuilder; + +public class FreeRdpClient implements ExternalApplicationType.PathApplication, ExternalRdpClient { + + @Override + public void launch(RdpLaunchConfig configuration) throws Exception { + var file = writeRdpConfigFile(configuration.getTitle(), configuration.getConfig()); + var b = CommandBuilder.of().addFile(file.toString()).add("/cert-ignore"); + if (configuration.getPassword() != null) { + var escapedPw = configuration.getPassword().getSecretValue().replaceAll("'", "\\\\'"); + b.add("/p:'" + escapedPw + "'"); + } + launch(b); + } + + @Override + public boolean supportsPasswordPassing() { + return true; + } + + @Override + public String getExecutable() { + return "xfreerdp"; + } + + @Override + public boolean isExplicitlyAsync() { + return true; + } + + @Override + public String getId() { + return "app.xfreeRdp"; + } +} diff --git a/app/src/main/java/io/xpipe/app/rdp/MstscRdpClient.java b/app/src/main/java/io/xpipe/app/rdp/MstscRdpClient.java new file mode 100644 index 000000000..93b08a56d --- /dev/null +++ b/app/src/main/java/io/xpipe/app/rdp/MstscRdpClient.java @@ -0,0 +1,75 @@ +package io.xpipe.app.rdp; + +import io.xpipe.app.prefs.ExternalApplicationType; +import io.xpipe.app.util.LocalShell; +import io.xpipe.app.util.RdpConfig; +import io.xpipe.app.util.ThreadHelper; +import io.xpipe.core.process.CommandBuilder; +import io.xpipe.core.util.SecretValue; +import org.apache.commons.io.FileUtils; + +import java.util.Map; + +public class MstscRdpClient implements ExternalApplicationType.PathApplication, ExternalRdpClient{ + + @Override + public void launch(RdpLaunchConfig configuration) throws Exception { + var adaptedRdpConfig = getAdaptedConfig(configuration); + var file = writeRdpConfigFile(configuration.getTitle(), adaptedRdpConfig); + LocalShell.getShell().executeSimpleCommand(CommandBuilder.of().add(getExecutable()).addFile(file.toString())); + ThreadHelper.runFailableAsync(() -> { + ThreadHelper.sleep(1000); + FileUtils.deleteQuietly(file.toFile()); + }); + } + + @Override + public boolean supportsPasswordPassing() { + return true; + } + + private RdpConfig getAdaptedConfig(RdpLaunchConfig configuration) throws Exception { + var input = configuration.getConfig(); + if (input.get("password 51").isPresent()) { + return input; + } + + if (input.get("username").isEmpty()) { + // return input; + } + + var pass = configuration.getPassword(); + if (pass == null) { + return input; + } + + var adapted = input.overlay( + Map.of("password 51", new RdpConfig.TypedValue("b", encrypt(pass)), "prompt for credentials", new RdpConfig.TypedValue("i", "0"))); + return adapted; + } + + private String encrypt(SecretValue password) throws Exception { + var ps = LocalShell.getLocalPowershell(); + var cmd = ps.command(CommandBuilder.of() + .add(sc -> "(" + + sc.getShellDialect().literalArgument(password.getSecretValue()) + + " | ConvertTo-SecureString -AsPlainText -Force) | ConvertFrom-SecureString")); + cmd.sensitive(); + return cmd.readStdoutOrThrow(); + } + + @Override + public String getExecutable() { + return "mstsc.exe"; + } + + @Override + public boolean isExplicitlyAsync() { + return false; + } + + @Override + public String getId() { + return "app.mstsc"; + } +} diff --git a/app/src/main/java/io/xpipe/app/rdp/RdpLaunchConfig.java b/app/src/main/java/io/xpipe/app/rdp/RdpLaunchConfig.java new file mode 100644 index 000000000..e71df3ba6 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/rdp/RdpLaunchConfig.java @@ -0,0 +1,15 @@ +package io.xpipe.app.rdp; + +import io.xpipe.app.util.RdpConfig; +import io.xpipe.core.util.SecretValue; +import lombok.Value; + +import java.util.UUID; + +@Value +public class RdpLaunchConfig { + String title; + RdpConfig config; + UUID storeId; + SecretValue password; +} diff --git a/app/src/main/java/io/xpipe/app/rdp/RemminaRdpClient.java b/app/src/main/java/io/xpipe/app/rdp/RemminaRdpClient.java new file mode 100644 index 000000000..0d9ac6efc --- /dev/null +++ b/app/src/main/java/io/xpipe/app/rdp/RemminaRdpClient.java @@ -0,0 +1,113 @@ +package io.xpipe.app.rdp; + +import io.xpipe.app.prefs.ExternalApplicationType; +import io.xpipe.app.util.LocalShell; +import io.xpipe.app.util.RdpConfig; +import io.xpipe.app.util.ShellTemp; +import io.xpipe.app.util.ThreadHelper; +import io.xpipe.core.process.CommandBuilder; +import io.xpipe.core.process.OsType; +import io.xpipe.core.util.SecretValue; +import org.apache.commons.io.FileUtils; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +public class RemminaRdpClient implements ExternalApplicationType.PathApplication, ExternalRdpClient { + + private List toStrip() { + return List.of("auto connect", "password 51", "prompt for credentials", "smart sizing"); + } + + @Override + public void launch(RdpLaunchConfig configuration) throws Exception { + RdpConfig c = configuration.getConfig(); + var l = new HashSet<>(c.getContent().keySet()); + toStrip().forEach(l::remove); + if (l.size() == 2 && l.contains("username") && l.contains("full address")) { + var encrypted = encryptPassword(configuration.getPassword()); + if (encrypted.isPresent()) { + var file = writeRemminaConfigFile(configuration, encrypted.get()); + launch(CommandBuilder.of().add("-c").addFile(file.toString())); + ThreadHelper.runFailableAsync(() -> { + ThreadHelper.sleep(5000); + FileUtils.deleteQuietly(file.toFile()); + }); + return; + } + } + + var file = writeRdpConfigFile(configuration.getTitle(), c); + launch(CommandBuilder.of().add("-c").addFile(file.toString())); + } + + private Optional encryptPassword(SecretValue password) throws Exception { + if (password == null) { + return Optional.empty(); + } + + try (var sc = LocalShell.getShell().start()) { + var prefSecretBase64 = sc.command("sed -n 's/^secret=//p' ~/.config/remmina/remmina.pref").readStdoutIfPossible(); + if (prefSecretBase64.isEmpty()) { + return Optional.empty(); + } + + var paddedPassword = password.getSecretValue(); + paddedPassword = paddedPassword + "\0".repeat(8 - paddedPassword.length() % 8); + var prefSecret = Base64.getDecoder().decode(prefSecretBase64.get()); + var key = Arrays.copyOfRange(prefSecret, 0, 24); + var iv = Arrays.copyOfRange(prefSecret, 24, prefSecret.length); + + var cipher = Cipher.getInstance("DESede/CBC/Nopadding"); + var keySpec = new SecretKeySpec(key, "DESede"); + var ivspec = new IvParameterSpec(iv); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivspec); + byte[] encryptedText = cipher.doFinal(paddedPassword.getBytes(StandardCharsets.UTF_8)); + var base64Encrypted = Base64.getEncoder().encodeToString(encryptedText); + return Optional.ofNullable(base64Encrypted); + } + } + + private Path writeRemminaConfigFile(RdpLaunchConfig configuration, String password) throws Exception { + var name = OsType.getLocal().makeFileSystemCompatible(configuration.getTitle()); + var file = ShellTemp.getLocalTempDataDirectory("rdp").resolve(name + ".remmina"); + var string = """ + [remmina] + protocol=RDP + name=%s + username=%s + server=%s + password=%s + cert_ignore=1 + """.formatted(configuration.getTitle(), configuration.getConfig().get("username").orElseThrow().getValue(), + configuration.getConfig().get("full address").orElseThrow().getValue(), password); + Files.createDirectories(file.getParent()); + Files.writeString(file, string); + return file; + } + + @Override + public boolean supportsPasswordPassing() { + return false; + } + + @Override + public String getExecutable() { + return "remmina"; + } + + @Override + public boolean isExplicitlyAsync() { + return true; + } + + @Override + public String getId() { + return "app.remmina"; + } +} diff --git a/app/src/main/java/io/xpipe/app/rdp/RemoteDesktopAppRdpClient.java b/app/src/main/java/io/xpipe/app/rdp/RemoteDesktopAppRdpClient.java new file mode 100644 index 000000000..99f34f178 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/rdp/RemoteDesktopAppRdpClient.java @@ -0,0 +1,32 @@ +package io.xpipe.app.rdp; + +import io.xpipe.app.prefs.ExternalApplicationType; +import io.xpipe.app.util.LocalShell; +import io.xpipe.core.process.CommandBuilder; + +public class RemoteDesktopAppRdpClient implements ExternalApplicationType.MacApplication, ExternalRdpClient { + + @Override + public void launch(RdpLaunchConfig configuration) throws Exception { + var file = writeRdpConfigFile(configuration.getTitle(), configuration.getConfig()); + LocalShell.getShell().executeSimpleCommand(CommandBuilder.of() + .add("open", "-a") + .addQuoted("Microsoft Remote Desktop.app") + .addFile(file.toString())); + } + + @Override + public boolean supportsPasswordPassing() { + return false; + } + + @Override + public String getApplicationName() { + return "Microsoft Remote Desktop"; + } + + @Override + public String getId() { + return "app.microsoftRemoteDesktopApp"; + } +} diff --git a/app/src/main/java/io/xpipe/app/rdp/WindowsAppRdpClient.java b/app/src/main/java/io/xpipe/app/rdp/WindowsAppRdpClient.java new file mode 100644 index 000000000..1ca4bcb0e --- /dev/null +++ b/app/src/main/java/io/xpipe/app/rdp/WindowsAppRdpClient.java @@ -0,0 +1,29 @@ +package io.xpipe.app.rdp; + +import io.xpipe.app.prefs.ExternalApplicationType; +import io.xpipe.app.util.LocalShell; +import io.xpipe.core.process.CommandBuilder; + +public class WindowsAppRdpClient implements ExternalApplicationType.MacApplication, ExternalRdpClient { + + @Override + public void launch(RdpLaunchConfig configuration) throws Exception { + var file = writeRdpConfigFile(configuration.getTitle(), configuration.getConfig()); + LocalShell.getShell().executeSimpleCommand(CommandBuilder.of().add("open", "-a").addQuoted("Windows App.app").addFile(file.toString())); + } + + @Override + public boolean supportsPasswordPassing() { + return false; + } + + @Override + public String getApplicationName() { + return "Windows App"; + } + + @Override + public String getId() { + return "app.windowsApp"; + } +} 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 d4349bcc7..a1bc0cae3 100644 --- a/app/src/main/java/io/xpipe/app/terminal/AlacrittyTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/AlacrittyTerminalType.java @@ -1,6 +1,7 @@ package io.xpipe.app.terminal; import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.prefs.ExternalApplicationType; import io.xpipe.app.util.LocalShell; import io.xpipe.core.process.CommandBuilder; @@ -30,14 +31,10 @@ public interface AlacrittyTerminalType extends ExternalTerminalType, TrackableTe return false; } - class Windows extends SimplePathType implements AlacrittyTerminalType { - - public Windows() { - super("app.alacritty", "alacritty", true); - } + class Windows implements ExternalApplicationType.PathApplication, ExternalTerminalType, AlacrittyTerminalType { @Override - protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) { + public void launch(TerminalLaunchConfiguration configuration) throws Exception { var b = CommandBuilder.of(); // if (configuration.getColor() != null) { @@ -48,33 +45,72 @@ public interface AlacrittyTerminalType extends ExternalTerminalType, TrackableTe // Alacritty is bugged and will not accept arguments with spaces even if they are correctly passed/escaped // So this will not work when the script file has spaces - return b.add("-t") + b.add("-t") .addQuoted(configuration.getCleanTitle()) .add("-e") .add(configuration.getDialectLaunchCommand()); - } - } - - class Linux extends SimplePathType implements AlacrittyTerminalType { - - public Linux() { - super("app.alacritty", "alacritty", true); + launch(b); } @Override - protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) { - return CommandBuilder.of() - .add("-t") - .addQuoted(configuration.getCleanTitle()) - .add("-e") - .addFile(configuration.getScriptFile()); + public String getExecutable() { + return "alacritty"; + } + + @Override + public boolean isExplicitlyAsync() { + return true; + } + + @Override + public String getId() { + return "app.alacritty"; } } - class MacOs extends MacOsType implements AlacrittyTerminalType { + class Linux implements ExternalApplicationType.PathApplication, AlacrittyTerminalType { - public MacOs() { - super("app.alacritty", "Alacritty"); + @Override + public String getExecutable() { + return "alacritty"; + } + + @Override + public boolean isExplicitlyAsync() { + return true; + } + + @Override + public String getId() { + return "app.alacritty"; + } + + @Override + public void launch(TerminalLaunchConfiguration configuration) throws Exception { + var b = CommandBuilder.of() + .add("-t") + .addQuoted(configuration.getCleanTitle()) + .add("-e") + .addFile(configuration.getScriptFile());; + launch(b); + } + } + + class MacOs implements ExternalApplicationType.MacApplication, ExternalTerminalType, TrackableTerminalType { + + @Override + public TerminalOpenFormat getOpenFormat() { + return null; + } + + @Override + public boolean isRecommended() { + return false; + } + + @Override + public boolean useColoredTitle() { + return false; } @Override @@ -88,5 +124,15 @@ public interface AlacrittyTerminalType extends ExternalTerminalType, TrackableTe .add("-e") .addFile(configuration.getScriptFile())); } + + @Override + public String getId() { + return "app.alacritty"; + } + + @Override + public String getApplicationName() { + return "Alacritty"; + } } } 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 2e11fc902..ce230d4b4 100644 --- a/app/src/main/java/io/xpipe/app/terminal/CmdTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/CmdTerminalType.java @@ -1,14 +1,14 @@ package io.xpipe.app.terminal; import io.xpipe.app.ext.ProcessControlProvider; +import io.xpipe.app.prefs.ExternalApplicationType; +import io.xpipe.app.prefs.ExternalEditorType; import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.ShellDialects; -public class CmdTerminalType extends ExternalTerminalType.SimplePathType implements TrackableTerminalType { +import java.nio.file.Path; - public CmdTerminalType() { - super("app.cmd", "cmd.exe", true); - } +public class CmdTerminalType implements ExternalApplicationType.PathApplication, ExternalTerminalType, TrackableTerminalType { @Override public boolean supportsEscapes() { @@ -36,12 +36,32 @@ public class CmdTerminalType extends ExternalTerminalType.SimplePathType impleme return false; } - @Override - protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) { + private CommandBuilder toCommand(TerminalLaunchConfiguration configuration) { if (configuration.getScriptDialect().equals(ShellDialects.CMD)) { return CommandBuilder.of().add("/c").addFile(configuration.getScriptFile()); } return CommandBuilder.of().add("/c").add(configuration.getDialectLaunchCommand()); } + + @Override + public String getExecutable() { + return "cmd.exe"; + } + + @Override + public boolean isExplicitlyAsync() { + return true; + } + + @Override + public String getId() { + return "app.cmd"; + } + + @Override + public void launch(TerminalLaunchConfiguration configuration) throws Exception { + var args = toCommand(configuration); + launch(args); + } } 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 2a9cae482..d098db913 100644 --- a/app/src/main/java/io/xpipe/app/terminal/CustomTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/CustomTerminalType.java @@ -9,11 +9,7 @@ import io.xpipe.core.process.OsType; import java.util.Locale; -public class CustomTerminalType extends ExternalApplicationType implements ExternalTerminalType { - - public CustomTerminalType() { - super("app.custom"); - } +public class CustomTerminalType implements ExternalApplicationType, ExternalTerminalType { @Override public TerminalOpenFormat getOpenFormat() { @@ -55,4 +51,9 @@ public class CustomTerminalType extends ExternalApplicationType implements Exter public boolean isAvailable() { return true; } + + @Override + public String getId() { + return "app.custom"; + } } 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 f370bbb96..b2ae8e5b5 100644 --- a/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java @@ -5,13 +5,10 @@ import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.ext.ProcessControlProvider; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.ExternalApplicationType; -import io.xpipe.app.util.*; import io.xpipe.core.process.*; import lombok.Getter; -import java.io.IOException; -import java.nio.file.Path; import java.util.*; public interface ExternalTerminalType extends PrefsChoiceValue { @@ -19,7 +16,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { // ExternalTerminalType PUTTY = new WindowsType("app.putty","putty") { // // @Override - // protected Optional determineInstallation() { + // public Optional determineInstallation() { // try { // var r = WindowsRegistry.local().readValue(WindowsRegistry.HKEY_LOCAL_MACHINE, // "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\Xshell.exe"); @@ -577,73 +574,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue { 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 isRecommended() { - return false; - } - - @Override - public boolean useColoredTitle() { - return true; - } - - @Override - public void launch(TerminalLaunchConfiguration configuration) throws Exception { - LocalShell.getShell() - .executeSimpleCommand(CommandBuilder.of() - .add("open", "-a") - .addQuoted("Terminal.app") - .addFile(configuration.getScriptFile())); - } - }; - ExternalTerminalType ITERM2 = new MacOsType("app.iterm2", "iTerm") { - - @Override - public TerminalOpenFormat getOpenFormat() { - return TerminalOpenFormat.TABBED; - } - - @Override - public int getProcessHierarchyOffset() { - return 3; - } - - @Override - public String getWebsite() { - return "https://iterm2.com/"; - } - - @Override - public boolean isRecommended() { - return true; - } - - @Override - public boolean useColoredTitle() { - return true; - } - - @Override - public void launch(TerminalLaunchConfiguration configuration) throws Exception { - LocalShell.getShell() - .executeSimpleCommand(CommandBuilder.of() - .add("open", "-a") - .addQuoted("iTerm.app") - .addFile(configuration.getScriptFile())); - } - }; + ExternalTerminalType MACOS_TERMINAL = new MacOsTerminalType(); + ExternalTerminalType ITERM2 = new ITerm2TerminalType(); ExternalTerminalType CUSTOM = new CustomTerminalType(); List WINDOWS_TERMINALS = List.of( WindowsTerminalType.WINDOWS_TERMINAL_CANARY, @@ -781,60 +713,34 @@ public interface ExternalTerminalType extends PrefsChoiceValue { return true; } - default void launch(TerminalLaunchConfiguration configuration) throws Exception {} + void launch(TerminalLaunchConfiguration configuration) throws Exception; - abstract class WindowsType extends ExternalApplicationType.WindowsType implements ExternalTerminalType { + abstract class SimplePathType implements ExternalApplicationType.PathApplication, TrackableTerminalType { - public WindowsType(String id, String executable) { - super(id, executable); + @Getter + private final String id; + @Getter + private final String executable; + private final boolean async; + + public SimplePathType(String id, String executable, boolean async) { + this.id = id; + this.executable = executable; + this.async = async; } @Override - public void launch(TerminalLaunchConfiguration configuration) throws Exception { - var location = determineFromPath(); - if (location.isEmpty()) { - location = determineInstallation(); - if (location.isEmpty()) { - throw new IOException("Unable to find installation of " - + toTranslatedString().getValue()); - } - } - - execute(location.get(), configuration); - } - - protected abstract void execute(Path file, TerminalLaunchConfiguration configuration) throws Exception; - } - - abstract class MacOsType extends ExternalApplicationType.MacApplication - implements ExternalTerminalType, TrackableTerminalType { - - public MacOsType(String id, String applicationName) { - super(id, applicationName); - } - } - - @Getter - abstract class PathCheckType extends ExternalApplicationType.PathApplication implements ExternalTerminalType { - - public PathCheckType(String id, String executable, boolean explicitAsync) { - super(id, executable, explicitAsync); - } - } - - @Getter - abstract class SimplePathType extends PathCheckType implements TrackableTerminalType { - - public SimplePathType(String id, String executable, boolean explicitAsync) { - super(id, executable, explicitAsync); + public boolean isExplicitlyAsync() { + return async; } @Override public void launch(TerminalLaunchConfiguration configuration) throws Exception { var args = toCommand(configuration); - launch(configuration.getColoredTitle(), args); + launch(args); } protected abstract CommandBuilder toCommand(TerminalLaunchConfiguration configuration); } + } diff --git a/app/src/main/java/io/xpipe/app/terminal/GnomeConsoleType.java b/app/src/main/java/io/xpipe/app/terminal/GnomeConsoleType.java index 482425fc7..85164eb0d 100644 --- a/app/src/main/java/io/xpipe/app/terminal/GnomeConsoleType.java +++ b/app/src/main/java/io/xpipe/app/terminal/GnomeConsoleType.java @@ -1,12 +1,9 @@ package io.xpipe.app.terminal; +import io.xpipe.app.prefs.ExternalApplicationType; import io.xpipe.core.process.CommandBuilder; -public class GnomeConsoleType extends ExternalTerminalType.SimplePathType implements TrackableTerminalType { - - public GnomeConsoleType() { - super("app.gnomeConsole", "kgx", true); - } +public class GnomeConsoleType implements ExternalApplicationType.PathApplication, TrackableTerminalType { @Override public TerminalOpenFormat getOpenFormat() { @@ -29,11 +26,26 @@ public class GnomeConsoleType extends ExternalTerminalType.SimplePathType implem } @Override - protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) { + public void launch(TerminalLaunchConfiguration configuration) throws Exception { var toExecute = CommandBuilder.of() .addIf(configuration.isPreferTabs(), "--tab") .add("--") .add(configuration.getDialectLaunchCommand()); - return toExecute; + launch(toExecute); + } + + @Override + public String getExecutable() { + return "kgx"; + } + + @Override + public boolean isExplicitlyAsync() { + return true; + } + + @Override + public String getId() { + return "app.gnomeConsole"; } } 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 ad9d572ea..03c2658b3 100644 --- a/app/src/main/java/io/xpipe/app/terminal/GnomeTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/GnomeTerminalType.java @@ -1,16 +1,13 @@ package io.xpipe.app.terminal; import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.app.prefs.ExternalApplicationType; import io.xpipe.app.util.CommandSupport; import io.xpipe.app.util.LocalShell; import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.ShellControl; -public class GnomeTerminalType extends ExternalTerminalType.PathCheckType implements TrackableTerminalType { - - public GnomeTerminalType() { - super("app.gnomeTerminal", "gnome-terminal", true); - } +public class GnomeTerminalType implements ExternalApplicationType.PathApplication, TrackableTerminalType { @Override public TerminalOpenFormat getOpenFormat() { @@ -35,10 +32,10 @@ public class GnomeTerminalType extends ExternalTerminalType.PathCheckType implem @Override public void launch(TerminalLaunchConfiguration configuration) throws Exception { try (ShellControl pc = LocalShell.getShell()) { - CommandSupport.isInPathOrThrow(pc, executable, toTranslatedString().getValue(), null); + CommandSupport.isInPathOrThrow(pc, getExecutable(), toTranslatedString().getValue(), null); var toExecute = CommandBuilder.of() - .add(executable, "-v", "--title") + .add(getExecutable(), "-v", "--title") .addQuoted(configuration.getColoredTitle()) .add("--") .addFile(configuration.getScriptFile()) @@ -48,4 +45,19 @@ public class GnomeTerminalType extends ExternalTerminalType.PathCheckType implem pc.executeSimpleCommand(toExecute); } } + + @Override + public String getExecutable() { + return "gnome-terminal"; + } + + @Override + public boolean isExplicitlyAsync() { + return true; + } + + @Override + public String getId() { + return "app.gnomeTerminal"; + } } diff --git a/app/src/main/java/io/xpipe/app/terminal/ITerm2TerminalType.java b/app/src/main/java/io/xpipe/app/terminal/ITerm2TerminalType.java new file mode 100644 index 000000000..904e093a8 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/terminal/ITerm2TerminalType.java @@ -0,0 +1,51 @@ +package io.xpipe.app.terminal; + +import io.xpipe.app.prefs.ExternalApplicationType; +import io.xpipe.app.util.LocalShell; +import io.xpipe.core.process.CommandBuilder; + +public class ITerm2TerminalType implements ExternalApplicationType.MacApplication, TrackableTerminalType { + + @Override + public TerminalOpenFormat getOpenFormat() { + return TerminalOpenFormat.TABBED; + } + + @Override + public int getProcessHierarchyOffset() { + return 3; + } + + @Override + public String getWebsite() { + return "https://iterm2.com/"; + } + + @Override + public boolean isRecommended() { + return true; + } + + @Override + public boolean useColoredTitle() { + return true; + } + + @Override + public void launch(TerminalLaunchConfiguration configuration) throws Exception { + LocalShell.getShell().executeSimpleCommand(CommandBuilder.of() + .add("open", "-a") + .addQuoted("iTerm.app") + .addFile(configuration.getScriptFile())); + } + + @Override + public String getApplicationName() { + return "iTerm"; + } + + @Override + public String getId() { + return "app.iterm2"; + } +} 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 3a3a09581..14b7dd931 100644 --- a/app/src/main/java/io/xpipe/app/terminal/KittyTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/KittyTerminalType.java @@ -2,6 +2,7 @@ package io.xpipe.app.terminal; import io.xpipe.app.ext.ProcessControlProvider; import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.prefs.ExternalApplicationType; import io.xpipe.app.util.CommandSupport; import io.xpipe.app.util.LocalShell; import io.xpipe.app.util.ShellTemp; @@ -154,11 +155,7 @@ public interface KittyTerminalType extends ExternalTerminalType, TrackableTermin } } - class MacOs extends MacOsType implements KittyTerminalType { - - public MacOs() { - super("app.kitty", "kitty"); - } + class MacOs implements ExternalApplicationType.MacApplication, KittyTerminalType { @Override public int getProcessHierarchyOffset() { @@ -199,5 +196,15 @@ public interface KittyTerminalType extends ExternalTerminalType, TrackableTermin return true; } } + + @Override + public String getApplicationName() { + return "kitty"; + } + + @Override + public String getId() { + return "app.kitty"; + } } } diff --git a/app/src/main/java/io/xpipe/app/terminal/MacOsTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/MacOsTerminalType.java new file mode 100644 index 000000000..a8ffabe21 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/terminal/MacOsTerminalType.java @@ -0,0 +1,46 @@ +package io.xpipe.app.terminal; + +import io.xpipe.app.prefs.ExternalApplicationType; +import io.xpipe.app.util.LocalShell; +import io.xpipe.core.process.CommandBuilder; + +public class MacOsTerminalType implements ExternalApplicationType.MacApplication, TrackableTerminalType { + + @Override + public TerminalOpenFormat getOpenFormat() { + return TerminalOpenFormat.TABBED; + } + + @Override + public int getProcessHierarchyOffset() { + return 2; + } + + @Override + public boolean isRecommended() { + return false; + } + + @Override + public boolean useColoredTitle() { + return true; + } + + @Override + public void launch(TerminalLaunchConfiguration configuration) throws Exception { + LocalShell.getShell().executeSimpleCommand(CommandBuilder.of() + .add("open", "-a") + .addQuoted("Terminal.app") + .addFile(configuration.getScriptFile())); + } + + @Override + public String getApplicationName() { + return "Terminal"; + } + + @Override + public String getId() { + return "app.macosTerminal"; + } +} diff --git a/app/src/main/java/io/xpipe/app/terminal/MobaXTermTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/MobaXTermTerminalType.java index 6f648908e..585ffee1e 100644 --- a/app/src/main/java/io/xpipe/app/terminal/MobaXTermTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/MobaXTermTerminalType.java @@ -1,6 +1,7 @@ package io.xpipe.app.terminal; import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.prefs.ExternalApplicationType; import io.xpipe.app.util.*; import io.xpipe.core.process.CommandBuilder; @@ -8,11 +9,7 @@ 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"); - } +public class MobaXTermTerminalType implements ExternalApplicationType.WindowsType, ExternalTerminalType { @Override public TerminalOpenFormat getOpenFormat() { @@ -20,7 +17,12 @@ public class MobaXTermTerminalType extends ExternalTerminalType.WindowsType { } @Override - protected Optional determineInstallation() { + public String getExecutable() { + return "MobaXterm"; + } + + @Override + public Optional determineInstallation() { try { var r = WindowsRegistry.local() .readStringValueIfPresent( @@ -43,12 +45,7 @@ public class MobaXTermTerminalType extends ExternalTerminalType.WindowsType { } @Override - public String getWebsite() { - return "https://mobaxterm.mobatek.net/"; - } - - @Override - protected void execute(Path file, TerminalLaunchConfiguration configuration) throws Exception { + public void launch(TerminalLaunchConfiguration configuration) throws Exception { try (var sc = LocalShell.getShell()) { SshLocalBridge.init(); var b = SshLocalBridge.get(); @@ -70,10 +67,20 @@ public class MobaXTermTerminalType extends ExternalTerminalType.WindowsType { Files.writeString(Path.of(script.toString()), "#!/usr/bin/env bash\n" + rawCommand); var fixedFile = script.toString().replaceAll("\\\\", "/").replaceAll("\\s", "\\$0"); sc.command(CommandBuilder.of() - .addFile(file.toString()) + .addFile(findExecutable()) .add("-newtab") .add(fixedFile)) .execute(); } } + + @Override + public String getWebsite() { + return "https://mobaxterm.mobatek.net/"; + } + + @Override + public String getId() { + return "app.mobaXterm"; + } } 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 080abd56a..cf9d68d44 100644 --- a/app/src/main/java/io/xpipe/app/terminal/PowerShellTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/PowerShellTerminalType.java @@ -1,21 +1,23 @@ package io.xpipe.app.terminal; import io.xpipe.app.ext.ProcessControlProvider; +import io.xpipe.app.prefs.ExternalApplicationType; import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.ShellDialects; import java.nio.charset.StandardCharsets; import java.util.Base64; -public class PowerShellTerminalType extends ExternalTerminalType.SimplePathType implements TrackableTerminalType { +public class PowerShellTerminalType implements ExternalApplicationType.PathApplication, TrackableTerminalType { @Override public boolean supportsEscapes() { return false; } - public PowerShellTerminalType() { - super("app.powershell", "powershell", true); + @Override + public void launch(TerminalLaunchConfiguration configuration) throws Exception { + launch(toCommand(configuration)); } @Override @@ -39,7 +41,6 @@ public class PowerShellTerminalType extends ExternalTerminalType.SimplePathType return false; } - @Override protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) { if (configuration.getScriptDialect().equals(ShellDialects.POWERSHELL)) { return CommandBuilder.of() @@ -60,4 +61,19 @@ public class PowerShellTerminalType extends ExternalTerminalType.SimplePathType return "\"" + base64 + "\""; }); } + + @Override + public String getExecutable() { + return "powershell.exe"; + } + + @Override + public boolean isExplicitlyAsync() { + return true; + } + + @Override + public String getId() { + return "app.powershell"; + } } diff --git a/app/src/main/java/io/xpipe/app/terminal/PtyxisTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/PtyxisTerminalType.java index f1361bcc6..3717dd355 100644 --- a/app/src/main/java/io/xpipe/app/terminal/PtyxisTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/PtyxisTerminalType.java @@ -1,12 +1,9 @@ package io.xpipe.app.terminal; +import io.xpipe.app.prefs.ExternalApplicationType; import io.xpipe.core.process.CommandBuilder; -public class PtyxisTerminalType extends ExternalTerminalType.SimplePathType implements TrackableTerminalType { - - public PtyxisTerminalType() { - super("app.ptyxis", "ptyxis", true); - } +public class PtyxisTerminalType implements ExternalApplicationType.PathApplication, TrackableTerminalType { @Override public TerminalOpenFormat getOpenFormat() { @@ -29,12 +26,26 @@ public class PtyxisTerminalType extends ExternalTerminalType.SimplePathType impl } @Override - protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) { + public void launch(TerminalLaunchConfiguration configuration) throws Exception { var toExecute = CommandBuilder.of() .addIf(configuration.isPreferTabs(), "--tab") .addIf(!configuration.isPreferTabs(), "--new-window") .add("--") .add(configuration.getDialectLaunchCommand()); - return toExecute; + launch(toExecute); + } + @Override + public String getExecutable() { + return "ptyxis"; + } + + @Override + public boolean isExplicitlyAsync() { + return true; + } + + @Override + public String getId() { + return "app.ptyxis"; } } 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 a6dea668d..b4efcfa38 100644 --- a/app/src/main/java/io/xpipe/app/terminal/PwshTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/PwshTerminalType.java @@ -1,21 +1,33 @@ package io.xpipe.app.terminal; +import io.xpipe.app.prefs.ExternalApplicationType; import io.xpipe.core.process.CommandBuilder; import java.nio.charset.StandardCharsets; import java.util.Base64; -public class PwshTerminalType extends ExternalTerminalType.SimplePathType implements TrackableTerminalType { - - public PwshTerminalType() { - super("app.pwsh", "pwsh", true); - } +public class PwshTerminalType implements ExternalApplicationType.PathApplication, TrackableTerminalType { @Override public boolean supportsEscapes() { return false; } + @Override + public void launch(TerminalLaunchConfiguration configuration) throws Exception { + var b = CommandBuilder.of() + .add("-ExecutionPolicy", "Bypass") + .add("-EncodedCommand") + .add(sc -> { + // Fix for https://github.com/PowerShell/PowerShell/issues/18530#issuecomment-1325691850 + var c = "$env:PSModulePath=\"\";" + + configuration.getDialectLaunchCommand().buildBase(sc); + var base64 = Base64.getEncoder().encodeToString(c.getBytes(StandardCharsets.UTF_16LE)); + return "\"" + base64 + "\""; + }); + launch(b); + } + @Override public TerminalOpenFormat getOpenFormat() { return TerminalOpenFormat.NEW_WINDOW; @@ -37,16 +49,17 @@ public class PwshTerminalType extends ExternalTerminalType.SimplePathType implem } @Override - protected CommandBuilder toCommand(TerminalLaunchConfiguration configuration) { - return CommandBuilder.of() - .add("-ExecutionPolicy", "Bypass") - .add("-EncodedCommand") - .add(sc -> { - // Fix for https://github.com/PowerShell/PowerShell/issues/18530#issuecomment-1325691850 - var c = "$env:PSModulePath=\"\";" - + configuration.getDialectLaunchCommand().buildBase(sc); - var base64 = Base64.getEncoder().encodeToString(c.getBytes(StandardCharsets.UTF_16LE)); - return "\"" + base64 + "\""; - }); + public String getExecutable() { + return "pwsh.exe"; + } + + @Override + public boolean isExplicitlyAsync() { + return true; + } + + @Override + public String getId() { + return "app.pwsh"; } } diff --git a/app/src/main/java/io/xpipe/app/terminal/SecureCrtTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/SecureCrtTerminalType.java index c3b5f7773..cec9e986d 100644 --- a/app/src/main/java/io/xpipe/app/terminal/SecureCrtTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/SecureCrtTerminalType.java @@ -1,6 +1,7 @@ package io.xpipe.app.terminal; import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.prefs.ExternalApplicationType; import io.xpipe.app.util.LocalShell; import io.xpipe.app.util.SshLocalBridge; import io.xpipe.core.process.CommandBuilder; @@ -9,11 +10,7 @@ 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"); - } +public class SecureCrtTerminalType implements ExternalApplicationType.WindowsType, ExternalTerminalType { @Override public TerminalOpenFormat getOpenFormat() { @@ -21,7 +18,12 @@ public class SecureCrtTerminalType extends ExternalTerminalType.WindowsType { } @Override - protected Optional determineInstallation() { + public String getExecutable() { + return "SecureCRT"; + } + + @Override + public Optional determineInstallation() { try (var sc = LocalShell.getShell().start()) { var env = sc.executeSimpleStringCommand( sc.getShellDialect().getPrintEnvironmentVariableCommand("ProgramFiles")); @@ -48,17 +50,12 @@ public class SecureCrtTerminalType extends ExternalTerminalType.WindowsType { } @Override - public String getWebsite() { - return "https://www.vandyke.com/products/securecrt/"; - } - - @Override - protected void execute(Path file, TerminalLaunchConfiguration configuration) throws Exception { + public void launch(TerminalLaunchConfiguration configuration) throws Exception { try (var sc = LocalShell.getShell()) { SshLocalBridge.init(); var b = SshLocalBridge.get(); var command = CommandBuilder.of() - .addFile(file.toString()) + .addFile(findExecutable()) .add("/T") .add("/SSH2", "/ACCEPTHOSTKEYS", "/I") .addFile(b.getIdentityKey().toString()) @@ -69,4 +66,14 @@ public class SecureCrtTerminalType extends ExternalTerminalType.WindowsType { sc.executeSimpleCommand(command); } } + + @Override + public String getWebsite() { + return "https://www.vandyke.com/products/securecrt/"; + } + + @Override + public String getId() { + return "app.secureCrt"; + } } 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 3a4f671c2..f13a9ed17 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TabbyTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/TabbyTerminalType.java @@ -1,5 +1,6 @@ package io.xpipe.app.terminal; +import io.xpipe.app.prefs.ExternalApplicationType; import io.xpipe.app.util.LocalShell; import io.xpipe.app.util.WindowsRegistry; import io.xpipe.core.process.CommandBuilder; @@ -50,11 +51,7 @@ public interface TabbyTerminalType extends ExternalTerminalType, TrackableTermin return TerminalInitFunction.none(); } - class Windows extends ExternalTerminalType.WindowsType implements TabbyTerminalType { - - public Windows() { - super("app.tabby", "Tabby.exe"); - } + class Windows implements ExternalApplicationType.WindowsType, TabbyTerminalType { @Override public int getProcessHierarchyOffset() { @@ -67,13 +64,13 @@ public interface TabbyTerminalType extends ExternalTerminalType, TrackableTermin } @Override - protected void execute(Path file, TerminalLaunchConfiguration configuration) throws Exception { + public void launch(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? LocalShell.getShell() .executeSimpleCommand(CommandBuilder.of() - .addFile(file.toString()) + .addFile(findExecutable()) .add("run") .addFile(configuration.getScriptFile()) .discardAllOutput()); @@ -81,7 +78,7 @@ public interface TabbyTerminalType extends ExternalTerminalType, TrackableTermin // This is probably not going to work as it does not launch a bat file LocalShell.getShell() .executeSimpleCommand(CommandBuilder.of() - .addFile(file.toString()) + .addFile(findExecutable()) .add("run") .add(sc -> configuration .getDialectLaunchCommand() @@ -92,7 +89,12 @@ public interface TabbyTerminalType extends ExternalTerminalType, TrackableTermin } @Override - protected Optional determineInstallation() { + public String getExecutable() { + return "Tabby.exe"; + } + + @Override + public Optional determineInstallation() { var perUser = WindowsRegistry.local() .readStringValueIfPresent( WindowsRegistry.HKEY_CURRENT_USER, @@ -113,19 +115,20 @@ public interface TabbyTerminalType extends ExternalTerminalType, TrackableTermin .map(Path::of); return systemWide; } + + @Override + public String getId() { + return "app.tabby"; + } } - class MacOs extends MacOsType implements TabbyTerminalType { + class MacOs implements ExternalApplicationType.MacApplication, TabbyTerminalType { @Override public boolean isRecommended() { return true; } - public MacOs() { - super("app.tabby", "Tabby"); - } - @Override public void launch(TerminalLaunchConfiguration configuration) throws Exception { LocalShell.getShell() @@ -135,5 +138,15 @@ public interface TabbyTerminalType extends ExternalTerminalType, TrackableTermin .add("-n", "--args", "run") .addFile(configuration.getScriptFile())); } + + @Override + public String getApplicationName() { + return "Tabby"; + } + + @Override + public String getId() { + return "app.tabby"; + } } } diff --git a/app/src/main/java/io/xpipe/app/terminal/TrackableTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/TrackableTerminalType.java index fd0ad0e43..b3fd2f42d 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TrackableTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/TrackableTerminalType.java @@ -1,6 +1,6 @@ package io.xpipe.app.terminal; -public interface TrackableTerminalType { +public interface TrackableTerminalType extends ExternalTerminalType { default int getProcessHierarchyOffset() { return 0; diff --git a/app/src/main/java/io/xpipe/app/terminal/WarpTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/WarpTerminalType.java index c4ef9b9df..0e93ef7cd 100644 --- a/app/src/main/java/io/xpipe/app/terminal/WarpTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/WarpTerminalType.java @@ -1,5 +1,6 @@ package io.xpipe.app.terminal; +import io.xpipe.app.prefs.ExternalApplicationType; import io.xpipe.app.util.*; import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.ShellDialects; @@ -91,11 +92,7 @@ public interface WarpTerminalType extends ExternalTerminalType, TrackableTermina } } - class MacOs extends MacOsType implements WarpTerminalType { - - public MacOs() { - super("app.warp", "Warp"); - } + class MacOs implements ExternalApplicationType.MacApplication, WarpTerminalType { @Override public int getProcessHierarchyOffset() { @@ -115,6 +112,16 @@ public interface WarpTerminalType extends ExternalTerminalType, TrackableTermina public TerminalOpenFormat getOpenFormat() { return TerminalOpenFormat.TABBED; } + + @Override + public String getApplicationName() { + return "Warp"; + } + + @Override + public String getId() { + return "app.warp"; + } } @Override 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 ebfa5e012..8302943bd 100644 --- a/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java @@ -33,11 +33,7 @@ public interface WezTerminalType extends ExternalTerminalType, TrackableTerminal return true; } - class Windows extends WindowsType implements WezTerminalType { - - public Windows() { - super("app.wezterm", "wezterm-gui"); - } + class Windows implements ExternalApplicationType.WindowsType, ExternalTerminalType, WezTerminalType { @Override public TerminalOpenFormat getOpenFormat() { @@ -45,16 +41,21 @@ public interface WezTerminalType extends ExternalTerminalType, TrackableTerminal } @Override - protected void execute(Path file, TerminalLaunchConfiguration configuration) throws Exception { + public void launch(TerminalLaunchConfiguration configuration) throws Exception { LocalShell.getShell() .executeSimpleCommand(CommandBuilder.of() - .addFile(file.toString()) + .addFile(findExecutable()) .add("start") .add(configuration.getDialectLaunchCommand())); } @Override - protected Optional determineInstallation() { + public String getExecutable() { + return "wezterm-gui"; + } + + @Override + public Optional determineInstallation() { try { var foundKey = WindowsRegistry.local() .findKeyForEqualValueMatchRecursive( @@ -83,13 +84,14 @@ public interface WezTerminalType extends ExternalTerminalType, TrackableTerminal return Optional.empty(); } + + @Override + public String getId() { + return "app.wezterm"; + } } - class Linux extends ExternalApplicationType implements WezTerminalType { - - public Linux() { - super("app.wezterm"); - } + class Linux implements ExternalApplicationType, WezTerminalType { @Override public TerminalOpenFormat getOpenFormat() { @@ -106,6 +108,11 @@ public interface WezTerminalType extends ExternalTerminalType, TrackableTerminal } } + @Override + public String getId() { + return "app.wezterm"; + } + @Override public void launch(TerminalLaunchConfiguration configuration) throws Exception { var spawn = LocalShell.getShell() @@ -121,11 +128,7 @@ public interface WezTerminalType extends ExternalTerminalType, TrackableTerminal } } - class MacOs extends MacOsType implements WezTerminalType { - - public MacOs() { - super("app.wezterm", "WezTerm"); - } + class MacOs implements ExternalApplicationType.MacApplication, WezTerminalType { @Override public TerminalOpenFormat getOpenFormat() { @@ -137,7 +140,7 @@ public interface WezTerminalType extends ExternalTerminalType, TrackableTerminal try (var sc = LocalShell.getShell()) { var pathOut = sc.command(String.format( "mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications 2>/dev/null", - applicationName)) + getApplicationName())) .readStdoutOrThrow(); var path = Path.of(pathOut); var spawn = sc.command(CommandBuilder.of() @@ -159,5 +162,15 @@ public interface WezTerminalType extends ExternalTerminalType, TrackableTerminal } } } + + @Override + public String getApplicationName() { + return "WezTerm"; + } + + @Override + public String getId() { + return "app.wezterm"; + } } } diff --git a/app/src/main/java/io/xpipe/app/terminal/XShellTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/XShellTerminalType.java index 7cf623f7f..a034d65a2 100644 --- a/app/src/main/java/io/xpipe/app/terminal/XShellTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/XShellTerminalType.java @@ -6,6 +6,7 @@ import io.xpipe.app.comp.base.ModalOverlay; import io.xpipe.app.core.AppCache; import io.xpipe.app.core.AppI18n; import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.prefs.ExternalApplicationType; import io.xpipe.app.util.LocalShell; import io.xpipe.app.util.SshLocalBridge; import io.xpipe.app.util.WindowsRegistry; @@ -14,11 +15,7 @@ import io.xpipe.core.process.CommandBuilder; import java.nio.file.Path; import java.util.Optional; -public class XShellTerminalType extends ExternalTerminalType.WindowsType { - - public XShellTerminalType() { - super("app.xShell", "Xshell"); - } +public class XShellTerminalType implements ExternalApplicationType.WindowsType, ExternalTerminalType { @Override public TerminalOpenFormat getOpenFormat() { @@ -26,7 +23,12 @@ public class XShellTerminalType extends ExternalTerminalType.WindowsType { } @Override - protected Optional determineInstallation() { + public String getExecutable() { + return "Xshell"; + } + + @Override + public Optional determineInstallation() { try { var r = WindowsRegistry.local() .readStringValueIfPresent( @@ -55,7 +57,7 @@ public class XShellTerminalType extends ExternalTerminalType.WindowsType { } @Override - protected void execute(Path file, TerminalLaunchConfiguration configuration) throws Exception { + public void launch(TerminalLaunchConfiguration configuration) throws Exception { SshLocalBridge.init(); if (!showInfo()) { return; @@ -65,7 +67,7 @@ public class XShellTerminalType extends ExternalTerminalType.WindowsType { var b = SshLocalBridge.get(); var keyName = b.getIdentityKey().getFileName().toString(); var command = CommandBuilder.of() - .addFile(file.toString()) + .addFile(findExecutable()) .add("-url") .addQuoted("ssh://" + b.getUser() + "@localhost:" + b.getPort()) .add("-i", keyName); @@ -90,4 +92,9 @@ public class XShellTerminalType extends ExternalTerminalType.WindowsType { modal.showAndWait(); return AppCache.getBoolean("xshellSetup", false); } + + @Override + public String getId() { + return "app.xShell"; + } } diff --git a/app/src/main/java/io/xpipe/app/util/AppJacksonModule.java b/app/src/main/java/io/xpipe/app/util/AppJacksonModule.java index d4184bf76..603ec7b6d 100644 --- a/app/src/main/java/io/xpipe/app/util/AppJacksonModule.java +++ b/app/src/main/java/io/xpipe/app/util/AppJacksonModule.java @@ -1,7 +1,7 @@ package io.xpipe.app.util; import io.xpipe.app.ext.LocalStore; -import io.xpipe.app.password.PasswordManager; +import io.xpipe.app.pwman.PasswordManager; import io.xpipe.app.storage.*; import io.xpipe.app.terminal.ExternalTerminalType; import io.xpipe.app.terminal.TerminalMultiplexer; diff --git a/app/src/main/java/io/xpipe/app/util/LocalShellCache.java b/app/src/main/java/io/xpipe/app/util/LocalShellCache.java index 7f06bf551..18c3b3f05 100644 --- a/app/src/main/java/io/xpipe/app/util/LocalShellCache.java +++ b/app/src/main/java/io/xpipe/app/util/LocalShellCache.java @@ -28,7 +28,7 @@ public class LocalShellCache extends ShellControlCache { .map(s -> s.asLocalPath()); } case OsType.Windows windows -> { - yield ExternalEditorType.VSCODE_WINDOWS.findExecutable(); + yield ExternalEditorType.VSCODE_WINDOWS.determineFromPath().or(() -> ExternalEditorType.VSCODE_WINDOWS.determineInstallation()); } }; set("codePath", app.orElse(null)); diff --git a/app/src/main/java/io/xpipe/app/vnc/ExternalVncClient.java b/app/src/main/java/io/xpipe/app/vnc/ExternalVncClient.java new file mode 100644 index 000000000..8f8252034 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/vnc/ExternalVncClient.java @@ -0,0 +1,46 @@ +package io.xpipe.app.vnc; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.xpipe.app.pwman.*; +import io.xpipe.core.process.OsType; +import io.xpipe.core.util.SecretValue; +import lombok.Value; + +import java.util.ArrayList; +import java.util.List; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +public interface ExternalVncClient { + + static List> getClasses() { + var l = new ArrayList>(); + l.add(OnePasswordManager.class); + l.add(KeePassXcPasswordManager.class); + l.add(BitwardenPasswordManager.class); + l.add(DashlanePasswordManager.class); + if (OsType.getLocal() != OsType.WINDOWS) { + l.add(LastpassPasswordManager.class); + l.add(EnpassPasswordManager.class); + } + l.add(KeeperPasswordManager.class); + l.add(PsonoPasswordManager.class); + if (OsType.getLocal() == OsType.WINDOWS) { + l.add(WindowsCredentialManager.class); + } + l.add(PasswordManagerCommand.class); + return l; + } + + @Value + class LaunchConfiguration { + String title; + String host; + int port; + SecretValue password; + } + + boolean isAvailable(); + + void launch(LaunchConfiguration configuration) throws Exception; + +} diff --git a/app/src/main/java/io/xpipe/app/vnc/TightVncClient.java b/app/src/main/java/io/xpipe/app/vnc/TightVncClient.java new file mode 100644 index 000000000..681640a1a --- /dev/null +++ b/app/src/main/java/io/xpipe/app/vnc/TightVncClient.java @@ -0,0 +1,47 @@ +package io.xpipe.app.vnc; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.app.prefs.ExternalApplicationType; +import io.xpipe.app.util.LocalShell; +import io.xpipe.core.process.CommandBuilder; +import lombok.Builder; +import lombok.extern.jackson.Jacksonized; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; + +@Builder +@Jacksonized +@JsonTypeName("tightVnc") +public class TightVncClient implements ExternalApplicationType.WindowsType, ExternalVncClient { + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public String getExecutable() { + return "tvnviewer.exe"; + } + + @Override + public Optional determineInstallation() { + return Optional.of(Path.of(System.getenv("PROGRAMFILES")) + .resolve("TightVNC") + .resolve("tvnviewer.exe")) + .filter(path -> Files.exists(path)); + } + + @Override + public void launch(LaunchConfiguration configuration) throws Exception { + var command = CommandBuilder.of().addFile(findExecutable()).add("-host").addLiteral(configuration.getHost()).add("-port").add("" + configuration.getPort()); + LocalShell.getShell().command(command).execute(); + } + + @Override + public String getId() { + return ""; + } +} diff --git a/app/src/main/java/module-info.java b/app/src/main/java/module-info.java index 0290a1e58..bff18d757 100644 --- a/app/src/main/java/module-info.java +++ b/app/src/main/java/module-info.java @@ -40,7 +40,8 @@ open module io.xpipe.app { exports io.xpipe.app.resources; exports io.xpipe.app.comp; exports io.xpipe.app.icon; - exports io.xpipe.app.password; + exports io.xpipe.app.pwman; + exports io.xpipe.app.rdp; requires com.sun.jna; requires com.sun.jna.platform;