This commit is contained in:
crschnick
2025-06-02 19:28:21 +00:00
parent 9a7d72f2b6
commit 6018a7bf76
55 changed files with 1340 additions and 829 deletions
@@ -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;
@@ -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<ExternalTerminalType> terminalType =
mapLocal(new SimpleObjectProperty<>(), "terminalType", ExternalTerminalType.class, false);
final ObjectProperty<ExternalRdpClientType> rdpClientType =
mapLocal(new SimpleObjectProperty<>(), "rdpClientType", ExternalRdpClientType.class, false);
final ObjectProperty<ExternalRdpClient> 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<ExternalRdpClientType> rdpClientType() {
public ObservableValue<ExternalRdpClient> 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);
@@ -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<Path> findApp() throws Exception {
String getApplicationName();
default Optional<Path> 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<Path> determineInstallation();
protected abstract Optional<Path> determineInstallation();
protected Optional<Path> determineFromPath() {
default Optional<Path> 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);
}
}
@@ -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<Path> determineInstallation() {
public String getId() {
return "app.notepad";
}
@Override
public boolean detach() {
return false;
}
@Override
public String getExecutable() {
return "notepad";
}
@Override
public Optional<Path> 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<Path> determineInstallation() {
public String getId() {
return "app.vscodium";
}
@Override
public boolean detach() {
return false;
}
@Override
public String getExecutable() {
return "codium.cmd";
}
@Override
public Optional<Path> 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<Path> determineInstallation() {
public String getId() {
return "app.cursor";
}
@Override
public boolean detach() {
return true;
}
@Override
public String getExecutable() {
return "Cursor";
}
@Override
public Optional<Path> 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<Path> determineInstallation() {
public String getId() {
return "app.void";
}
@Override
public boolean detach() {
return true;
}
@Override
public String getExecutable() {
return "Void";
}
@Override
public Optional<Path> 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<Path> determineInstallation() {
public String getId() {
return "app.windsurf";
}
@Override
public boolean detach() {
return false;
}
@Override
public String getExecutable() {
return "windsurf.cmd";
}
@Override
public Optional<Path> 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<Path> determineInstallation() {
public String getId() {
return "app.theiaide";
}
@Override
public boolean detach() {
return true;
}
@Override
public String getExecutable() {
return "Theiaide";
}
@Override
public Optional<Path> 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<Path> determineInstallation() {
public String getId() {
return "app.trae";
}
@Override
public boolean detach() {
return false;
}
@Override
public String getExecutable() {
return "trae.cmd";
}
@Override
public Optional<Path> 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<Path> determineInstallation() {
public String getId() {
return "app.vscode";
}
@Override
public boolean detach() {
return false;
}
@Override
public String getExecutable() {
return "code.cmd";
}
@Override
public Optional<Path> 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<Path> determineInstallation() {
public String getId() {
return "app.vscodeInsiders";
}
@Override
public boolean detach() {
return false;
}
@Override
public String getExecutable() {
return "code-insiders.cmd";
}
@Override
public Optional<Path> 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<Path> determineInstallation() {
public String getId() {
return "app.notepad++";
}
@Override
public boolean detach() {
return false;
}
@Override
public String getExecutable() {
return "notepad++";
}
@Override
public Optional<Path> 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<Path> findExecutable() {
var location = determineFromPath();
if (location.isEmpty()) {
location = determineInstallation();
}
return location;
}
}
}
@@ -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<Path> 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<ExternalRdpClientType> WINDOWS_CLIENTS = List.of(MSTSC, DEVOLUTIONS);
List<ExternalRdpClientType> LINUX_CLIENTS = List.of(REMMINA, X_FREE_RDP);
List<ExternalRdpClientType> MACOS_CLIENTS = List.of(MICROSOFT_REMOTE_DESKTOP_MACOS_APP, WINDOWS_APP_MACOS);
@SuppressWarnings("TrivialFunctionalExpressionUsage")
List<ExternalRdpClientType> ALL = ((Supplier<List<ExternalRdpClientType>>) () -> {
var all = new ArrayList<ExternalRdpClientType>();
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<String> 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<String> 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;
}
}
}
@@ -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;
@@ -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();
}
}
@@ -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;
@@ -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;
@@ -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;
@@ -1,4 +1,4 @@
package io.xpipe.app.password;
package io.xpipe.app.pwman;
import io.xpipe.core.util.InPlaceSecretValue;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -1,4 +1,4 @@
package io.xpipe.app.password;
package io.xpipe.app.pwman;
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.KeyGenerationParameters;
@@ -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;
@@ -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";
}
}
@@ -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<Path> 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";
}
}
@@ -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<ExternalRdpClient> WINDOWS_CLIENTS = List.of(MSTSC, DEVOLUTIONS);
List<ExternalRdpClient> LINUX_CLIENTS = List.of(REMMINA, X_FREE_RDP);
List<ExternalRdpClient> MACOS_CLIENTS = List.of(MICROSOFT_REMOTE_DESKTOP_MACOS_APP, WINDOWS_APP_MACOS);
@SuppressWarnings("TrivialFunctionalExpressionUsage")
List<ExternalRdpClient> ALL = ((Supplier<List<ExternalRdpClient>>) () -> {
var all = new ArrayList<ExternalRdpClient>();
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;
}
}
@@ -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";
}
}
@@ -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";
}
}
@@ -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;
}
@@ -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<String> 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<String> 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";
}
}
@@ -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";
}
}
@@ -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";
}
}
@@ -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";
}
}
}
@@ -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);
}
}
@@ -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";
}
}
@@ -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<Path> determineInstallation() {
// public Optional<Path> 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<ExternalTerminalType> 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);
}
}
@@ -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";
}
}
@@ -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";
}
}
@@ -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";
}
}
@@ -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";
}
}
}
@@ -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";
}
}
@@ -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<Path> determineInstallation() {
public String getExecutable() {
return "MobaXterm";
}
@Override
public Optional<Path> 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";
}
}
@@ -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";
}
}
@@ -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";
}
}
@@ -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";
}
}
@@ -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<Path> determineInstallation() {
public String getExecutable() {
return "SecureCRT";
}
@Override
public Optional<Path> 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";
}
}
@@ -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<Path> determineInstallation() {
public String getExecutable() {
return "Tabby.exe";
}
@Override
public Optional<Path> 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";
}
}
}
@@ -1,6 +1,6 @@
package io.xpipe.app.terminal;
public interface TrackableTerminalType {
public interface TrackableTerminalType extends ExternalTerminalType {
default int getProcessHierarchyOffset() {
return 0;
@@ -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
@@ -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<Path> determineInstallation() {
public String getExecutable() {
return "wezterm-gui";
}
@Override
public Optional<Path> 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";
}
}
}
@@ -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<Path> determineInstallation() {
public String getExecutable() {
return "Xshell";
}
@Override
public Optional<Path> 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";
}
}
@@ -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;
@@ -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));
@@ -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<Class<?>> getClasses() {
var l = new ArrayList<Class<?>>();
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;
}
@@ -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<Path> 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 "";
}
}
+2 -1
View File
@@ -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;