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 587bf31b..b1636253 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java @@ -137,10 +137,10 @@ public class AppPrefs { new AboutCategory(), new SystemCategory(), new AppearanceCategory(), - new SyncCategory(), - new VaultCategory(), new TerminalCategory(), new EditorCategory(), + new SyncCategory(), + new VaultCategory(), new LocalShellCategory(), new SecurityCategory(), new PasswordManagerCategory(), diff --git a/app/src/main/java/io/xpipe/app/prefs/TerminalCategory.java b/app/src/main/java/io/xpipe/app/prefs/TerminalCategory.java index a95fcad7..af2dbde9 100644 --- a/app/src/main/java/io/xpipe/app/prefs/TerminalCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/TerminalCategory.java @@ -5,9 +5,12 @@ import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.impl.ChoiceComp; +import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.StackComp; import io.xpipe.app.fxcomps.impl.TextFieldComp; +import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.terminal.ExternalTerminalType; +import io.xpipe.app.util.Hyperlinks; import io.xpipe.app.util.OptionsBuilder; import io.xpipe.app.util.TerminalLauncher; import io.xpipe.app.util.ThreadHelper; @@ -15,6 +18,8 @@ import io.xpipe.core.store.LocalStore; import javafx.beans.binding.Bindings; import javafx.geometry.Insets; import javafx.geometry.Pos; +import javafx.scene.control.ListCell; +import javafx.scene.paint.Color; import org.kordamp.ikonli.javafx.FontIcon; import java.util.List; @@ -26,6 +31,57 @@ public class TerminalCategory extends AppPrefsCategory { return "terminal"; } + private Comp terminalChoice() { + var prefs = AppPrefs.get(); + var c = ChoiceComp.ofTranslatable( + prefs.terminalType, PrefsChoiceValue.getSupported(ExternalTerminalType.class), false); + c.apply(struc -> { + struc.get().setCellFactory(param -> { + return new ListCell<>() { + @Override + protected void updateItem(ExternalTerminalType item, boolean empty) { + super.updateItem(item, empty); + if (empty) { + return; + } + + setText(item.toTranslatedString().getValue()); + if (item != ExternalTerminalType.CUSTOM) { + var graphic = new FontIcon(item.isRecommended() ? "mdi2c-check-decagram" : "mdi2a-alert-circle-check"); + graphic.setFill(item.isRecommended() ? Color.GREEN : Color.ORANGE); + setGraphic(graphic); + } else { + setGraphic(new FontIcon("mdi2m-minus-circle")); + } + } + }; + }); + }); + + var visit = new ButtonComp(AppI18n.observable("website"), new FontIcon("mdi2w-web"), () -> { + var t = prefs.terminalType().getValue(); + if (t == null || t.getWebsite() == null) { + return; + } + + Hyperlinks.open(t.getWebsite()); + }); + var visitVisible = BindingsHelper.persist(Bindings.createBooleanBinding(() -> { + var t = prefs.terminalType().getValue(); + if (t == null || t.getWebsite() == null) { + return false; + } + + return true; + }, prefs.terminalType())); + visit.visible(visitVisible); + + return new HorizontalComp(List.of(c, visit)).apply(struc -> { + struc.get().setAlignment(Pos.CENTER_LEFT); + struc.get().setSpacing(10); + }); + } + @Override protected Comp create() { var prefs = AppPrefs.get(); @@ -46,8 +102,7 @@ public class TerminalCategory extends AppPrefsCategory { .addTitle("terminalConfiguration") .sub(new OptionsBuilder() .nameAndDescription("terminalEmulator") - .addComp(ChoiceComp.ofTranslatable( - prefs.terminalType, PrefsChoiceValue.getSupported(ExternalTerminalType.class), false)) + .addComp(terminalChoice(), prefs.terminalType) .nameAndDescription("customTerminalCommand") .addComp(new TextFieldComp(prefs.customTerminalCommand, true) .apply(struc -> struc.get().setPromptText("myterminal -e $CMD")) diff --git a/app/src/main/java/io/xpipe/app/terminal/AlacrittyTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/AlacrittyTerminalType.java new file mode 100644 index 00000000..a80feebc --- /dev/null +++ b/app/src/main/java/io/xpipe/app/terminal/AlacrittyTerminalType.java @@ -0,0 +1,93 @@ +package io.xpipe.app.terminal; + +import io.xpipe.app.util.LocalShell; +import io.xpipe.core.process.CommandBuilder; + +public interface AlacrittyTerminalType extends ExternalTerminalType { + + static class Windows extends SimplePathType implements AlacrittyTerminalType { + + public Windows() { + super("app.alacritty", "alacritty", false); + } + + @Override + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + var b = CommandBuilder.of(); + +// if (configuration.getColor() != null) { +// b.add("-o") +// .addQuoted("colors.primary.background='%s'" +// .formatted(configuration.getColor().toHexString())); +// } + + // 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") + .addQuoted(configuration.getCleanTitle()) + .add("-e") + .add(configuration.getDialectLaunchCommand()); + } + + } + + static class Linux extends SimplePathType implements AlacrittyTerminalType { + + public Linux() { + super("app.alacritty", "alacritty", true); + } + + @Override + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + return CommandBuilder.of() + .add("-t") + .addQuoted(configuration.getCleanTitle()) + .add("-e") + .addFile(configuration.getScriptFile()); + } + + } + + ExternalTerminalType ALACRITTY_WINDOWS = new Windows(); + ExternalTerminalType ALACRITTY_LINUX = new Linux(); + ExternalTerminalType ALACRITTY_MAC_OS = new MacOs(); + + @Override + default String getWebsite() { + return "https://github.com/alacritty/alacritty"; + } + + @Override + default boolean isRecommended() { + return false; + } + + @Override + default boolean supportsTabs() { + return false; + } + + @Override + default boolean supportsColoredTitle() { + return false; + } + + class MacOs extends MacOsType implements AlacrittyTerminalType { + + public MacOs() { + super("app.alacritty", "Alacritty"); + } + + @Override + public void launch(LaunchConfiguration configuration) throws Exception { + LocalShell.getShell() + .executeSimpleCommand(CommandBuilder.of() + .add("open", "-a") + .addQuoted("Alacritty.app") + .add("-n", "--args", "-t") + .addQuoted(configuration.getCleanTitle()) + .add("-e") + .addFile(configuration.getScriptFile())); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/terminal/CustomTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/CustomTerminalType.java new file mode 100644 index 00000000..2209e425 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/terminal/CustomTerminalType.java @@ -0,0 +1,57 @@ +package io.xpipe.app.terminal; + +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.app.util.LocalShell; +import io.xpipe.core.process.OsType; + +import java.util.Locale; + +public class CustomTerminalType extends ExternalApplicationType implements ExternalTerminalType { + + public CustomTerminalType() { + super("app.custom"); + } + + @Override + public boolean supportsTabs() { + return true; + } + + @Override + public boolean isRecommended() { + return true; + } + + @Override + public boolean supportsColoredTitle() { + return true; + } + + @Override + public void launch(LaunchConfiguration configuration) throws Exception { + var custom = AppPrefs.get().customTerminalCommand().getValue(); + if (custom == null || custom.isBlank()) { + throw ErrorEvent.expected(new IllegalStateException("No custom terminal command specified")); + } + + var format = custom.toLowerCase(Locale.ROOT).contains("$cmd") ? custom : custom + " $CMD"; + try (var pc = LocalShell.getShell()) { + var toExecute = ExternalApplicationHelper.replaceFileArgument(format, "CMD", configuration.getScriptFile().toString()); + // We can't be sure whether the command is blocking or not, so always make it not blocking + if (pc.getOsType().equals(OsType.WINDOWS)) { + toExecute = "start \"" + configuration.getCleanTitle() + "\" " + toExecute; + } else { + toExecute = "nohup " + toExecute + " /dev/null & disown"; + } + pc.executeSimpleCommand(toExecute); + } + } + + @Override + public boolean isAvailable() { + return true; + } +} 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 46cf8742..02cd76c0 100644 --- a/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java @@ -1,9 +1,6 @@ package io.xpipe.app.terminal; import io.xpipe.app.ext.PrefsChoiceValue; -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.app.storage.DataStoreColor; import io.xpipe.app.util.*; @@ -23,6 +20,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue { ExternalTerminalType CMD = new SimplePathType("app.cmd", "cmd.exe", true) { + @Override + public boolean isRecommended() { + return false; + } + @Override public boolean supportsTabs() { return false; @@ -45,6 +47,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue { ExternalTerminalType POWERSHELL = new SimplePathType("app.powershell", "powershell", true) { + @Override + public boolean isRecommended() { + return false; + } + @Override public boolean supportsTabs() { return false; @@ -80,6 +87,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue { ExternalTerminalType PWSH = new SimplePathType("app.pwsh", "pwsh", true) { + @Override + public boolean isRecommended() { + return false; + } + @Override public boolean supportsTabs() { return false; @@ -104,121 +116,17 @@ public interface ExternalTerminalType extends PrefsChoiceValue { }); } }; - - ExternalTerminalType ALACRITTY_WINDOWS = new SimplePathType("app.alacritty", "alacritty", false) { - - @Override - public boolean supportsTabs() { - return false; - } - + ExternalTerminalType GNOME_TERMINAL = new PathCheckType("app.gnomeTerminal", "gnome-terminal", true) { @Override public boolean supportsColoredTitle() { - return false; - } - - @Override - protected CommandBuilder toCommand(LaunchConfiguration configuration) { - var b = CommandBuilder.of(); - if (configuration.getColor() != null) { - b.add("-o") - .addQuoted("colors.primary.background='%s'" - .formatted(configuration.getColor().toHexString())); - } - - // 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") - .addQuoted(configuration.getCleanTitle()) - .add("-e") - .add(configuration.getDialectLaunchCommand()); - } - }; - ExternalTerminalType TABBY_WINDOWS = new WindowsType("app.tabby", "Tabby") { - - @Override - public boolean supportsTabs() { return true; } @Override - protected void execute(Path file, LaunchConfiguration 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()) - .add("run") - .addFile(configuration.getScriptFile()) - .discardOutput()); - } - - LocalShell.getShell() - .executeSimpleCommand(CommandBuilder.of() - .addFile(file.toString()) - .add("run") - .add(configuration.getDialectLaunchCommand()) - .discardOutput()); - } - - @Override - protected Optional determineInstallation() { - var perUser = WindowsRegistry.readString( - WindowsRegistry.HKEY_CURRENT_USER, - "SOFTWARE\\71445fac-d6ef-5436-9da7-5a323762d7f5", - "InstallLocation") - .map(p -> p + "\\Tabby.exe") - .map(Path::of); - if (perUser.isPresent()) { - return perUser; - } - - var systemWide = WindowsRegistry.readString( - WindowsRegistry.HKEY_LOCAL_MACHINE, - "SOFTWARE\\71445fac-d6ef-5436-9da7-5a323762d7f5", - "InstallLocation") - .map(p -> p + "\\Tabby.exe") - .map(Path::of); - return systemWide; - } - }; - ExternalTerminalType WEZ_WINDOWS = new WindowsType("app.wezterm", "wezterm-gui") { - - @Override - public boolean supportsTabs() { + public boolean isRecommended() { return false; } - @Override - protected void execute(Path file, LaunchConfiguration configuration) throws Exception { - LocalShell.getShell().executeSimpleCommand(CommandBuilder.of().addFile(file.toString()).add("start").add(configuration.getDialectLaunchCommand())); - } - - @Override - protected Optional determineInstallation() { - Optional launcherDir; - launcherDir = WindowsRegistry.readString( - WindowsRegistry.HKEY_LOCAL_MACHINE, - "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{BCF6F0DA-5B9A-408D-8562-F680AE6E1EAF}_is1", - "InstallLocation") - .map(p -> p + "\\wezterm-gui.exe"); - return launcherDir.map(Path::of); - } - }; - ExternalTerminalType WEZ_LINUX = new SimplePathType("app.wezterm", "wezterm-gui", true) { - - @Override - public boolean supportsTabs() { - return false; - } - - @Override - protected CommandBuilder toCommand(LaunchConfiguration configuration) { - return CommandBuilder.of().add("start").addFile(configuration.getScriptFile()); - } - }; - ExternalTerminalType GNOME_TERMINAL = new PathCheckType("app.gnomeTerminal", "gnome-terminal", true) { - @Override public boolean supportsTabs() { return false; @@ -243,6 +151,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue { }; ExternalTerminalType KONSOLE = new SimplePathType("app.konsole", "konsole", true) { + @Override + public boolean isRecommended() { + return true; + } + @Override public boolean supportsTabs() { return true; @@ -262,6 +175,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } }; ExternalTerminalType XFCE = new SimplePathType("app.xfce", "xfce4-terminal", true) { + @Override + public boolean supportsColoredTitle() { + return true; + } + + + @Override + public boolean isRecommended() { + return true; + } @Override public boolean supportsTabs() { @@ -278,6 +201,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } }; ExternalTerminalType ELEMENTARY = new SimplePathType("app.elementaryTerminal", "io.elementary.terminal", true) { + @Override + public boolean supportsColoredTitle() { + return true; + } + + + @Override + public boolean isRecommended() { + return true; + } @Override public boolean supportsTabs() { @@ -290,6 +223,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } }; ExternalTerminalType TILIX = new SimplePathType("app.tilix", "tilix", true) { + @Override + public boolean supportsColoredTitle() { + return true; + } + + + @Override + public boolean isRecommended() { + return false; + } @Override public boolean supportsTabs() { @@ -306,6 +249,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } }; ExternalTerminalType TERMINATOR = new SimplePathType("app.terminator", "terminator", true) { + @Override + public boolean supportsColoredTitle() { + return true; + } + + + @Override + public boolean isRecommended() { + return true; + } @Override public boolean supportsTabs() { @@ -323,6 +276,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } }; ExternalTerminalType TERMINOLOGY = new SimplePathType("app.terminology", "terminology", true) { + @Override + public boolean supportsColoredTitle() { + return true; + } + + + @Override + public boolean isRecommended() { + return true; + } @Override public boolean supportsTabs() { @@ -340,6 +303,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } }; ExternalTerminalType COOL_RETRO_TERM = new SimplePathType("app.coolRetroTerm", "cool-retro-term", true) { + @Override + public boolean supportsColoredTitle() { + return true; + } + + + @Override + public boolean isRecommended() { + return false; + } @Override public boolean supportsTabs() { @@ -356,6 +329,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } }; ExternalTerminalType GUAKE = new SimplePathType("app.guake", "guake", true) { + @Override + public boolean supportsColoredTitle() { + return true; + } + + + @Override + public boolean isRecommended() { + return true; + } @Override public boolean supportsTabs() { @@ -372,28 +355,17 @@ public interface ExternalTerminalType extends PrefsChoiceValue { .addFile(configuration.getScriptFile()); } }; - ExternalTerminalType ALACRITTY_LINUX = new SimplePathType("app.alacritty", "alacritty", true) { - - @Override - public boolean supportsTabs() { - return false; - } - + ExternalTerminalType TILDA = new SimplePathType("app.tilda", "tilda", true) { @Override public boolean supportsColoredTitle() { - return false; + return true; } + @Override - protected CommandBuilder toCommand(LaunchConfiguration configuration) { - return CommandBuilder.of() - .add("-t") - .addQuoted(configuration.getCleanTitle()) - .add("-e") - .addFile(configuration.getScriptFile()); + public boolean isRecommended() { + return true; } - }; - ExternalTerminalType TILDA = new SimplePathType("app.tilda", "tilda", true) { @Override public boolean supportsTabs() { @@ -406,6 +378,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } }; ExternalTerminalType XTERM = new SimplePathType("app.xterm", "xterm", true) { + @Override + public boolean supportsColoredTitle() { + return true; + } + + + @Override + public boolean isRecommended() { + return false; + } @Override public boolean supportsTabs() { @@ -422,6 +404,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } }; ExternalTerminalType DEEPIN_TERMINAL = new SimplePathType("app.deepinTerminal", "deepin-terminal", true) { + @Override + public boolean supportsColoredTitle() { + return true; + } + + + @Override + public boolean isRecommended() { + return false; + } @Override public boolean supportsTabs() { @@ -434,6 +426,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } }; ExternalTerminalType Q_TERMINAL = new SimplePathType("app.qTerminal", "qterminal", true) { + @Override + public boolean supportsColoredTitle() { + return true; + } + + @Override + public boolean isRecommended() { + return false; + } + @Override public boolean supportsTabs() { @@ -446,6 +448,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } }; ExternalTerminalType MACOS_TERMINAL = new MacOsType("app.macosTerminal", "Terminal") { + @Override + public boolean supportsColoredTitle() { + return true; + } + + @Override + public boolean isRecommended() { + return false; + } + @Override public boolean supportsTabs() { return false; @@ -467,6 +479,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } }; ExternalTerminalType ITERM2 = new MacOsType("app.iterm2", "iTerm") { + @Override + public boolean supportsColoredTitle() { + return true; + } + + @Override + public boolean isRecommended() { + return true; + } + @Override public boolean supportsTabs() { return true; @@ -504,6 +526,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue { }; ExternalTerminalType WARP = new MacOsType("app.warp", "Warp") { + @Override + public boolean supportsColoredTitle() { + return true; + } + + @Override + public boolean isRecommended() { + return true; + } + @Override public boolean supportsTabs() { return true; @@ -523,79 +555,18 @@ public interface ExternalTerminalType extends PrefsChoiceValue { .addFile(configuration.getScriptFile())); } }; - ExternalTerminalType TABBY_MAC_OS = new MacOsType("app.tabby", "Tabby") { - @Override - public boolean supportsTabs() { - return true; - } - - @Override - public void launch(LaunchConfiguration configuration) throws Exception { - LocalShell.getShell() - .executeSimpleCommand(CommandBuilder.of() - .add("open", "-a") - .addQuoted("Tabby.app") - .add("-n", "--args", "run") - .addFile(configuration.getScriptFile())); - } - }; - ExternalTerminalType ALACRITTY_MACOS = new MacOsType("app.alacritty", "Alacritty") { - - @Override - public boolean supportsTabs() { - return false; - } - - @Override - public boolean supportsColoredTitle() { - return false; - } - - @Override - public void launch(LaunchConfiguration configuration) throws Exception { - LocalShell.getShell() - .executeSimpleCommand(CommandBuilder.of() - .add("open", "-a") - .addQuoted("Alacritty.app") - .add("-n", "--args", "-t") - .addQuoted(configuration.getCleanTitle()) - .add("-e") - .addFile(configuration.getScriptFile())); - } - }; - ExternalTerminalType WEZ_MACOS = new MacOsType("app.wezterm", "WezTerm") { - - @Override - public boolean supportsTabs() { - return false; - } - - @Override - public void launch(LaunchConfiguration configuration) throws Exception { - var c = CommandBuilder.of() - .addFile(getApplicationPath() - .orElseThrow() - .resolve("Contents") - .resolve("MacOS") - .resolve("wezterm-gui") - .toString()) - .add("start") - .add(configuration.getDialectLaunchCommand()); - ExternalApplicationHelper.startAsync(c); - } - }; - ExternalTerminalType CUSTOM = new CustomType(); + ExternalTerminalType CUSTOM = new CustomTerminalType(); List WINDOWS_TERMINALS = List.of( - TABBY_WINDOWS, - ALACRITTY_WINDOWS, - WEZ_WINDOWS, + TabbyTerminalType.TABBY_WINDOWS, + AlacrittyTerminalType.ALACRITTY_WINDOWS, + WezTerminalType.WEZTERM_WINDOWS, WindowsTerminalType.WINDOWS_TERMINAL_PREVIEW, WindowsTerminalType.WINDOWS_TERMINAL, CMD, PWSH, POWERSHELL); List LINUX_TERMINALS = List.of( - WEZ_LINUX, + WezTerminalType.WEZTERM_LINUX, KONSOLE, XFCE, ELEMENTARY, @@ -606,13 +577,13 @@ public interface ExternalTerminalType extends PrefsChoiceValue { TERMINOLOGY, COOL_RETRO_TERM, GUAKE, - ALACRITTY_LINUX, + AlacrittyTerminalType.ALACRITTY_LINUX, TILDA, XTERM, DEEPIN_TERMINAL, Q_TERMINAL); List MACOS_TERMINALS = - List.of(ITERM2, TABBY_MAC_OS, ALACRITTY_MACOS, KittyTerminalType.KITTY_MACOS, WARP, WEZ_MACOS, MACOS_TERMINAL); + List.of(ITERM2, TabbyTerminalType.TABBY_MAC_OS, AlacrittyTerminalType.ALACRITTY_MAC_OS, KittyTerminalType.KITTY_MACOS, WARP, WezTerminalType.WEZTERM_MAC_OS, MACOS_TERMINAL); @SuppressWarnings("TrivialFunctionalExpressionUsage") List ALL = ((Supplier>) () -> { @@ -653,10 +624,14 @@ public interface ExternalTerminalType extends PrefsChoiceValue { boolean supportsTabs(); - default boolean supportsColoredTitle() { - return true; + default String getWebsite() { + return null; } + boolean isRecommended(); + + boolean supportsColoredTitle(); + default boolean shouldClear() { return true; } @@ -709,43 +684,6 @@ public interface ExternalTerminalType extends PrefsChoiceValue { } } - class CustomType extends ExternalApplicationType implements ExternalTerminalType { - - public CustomType() { - super("app.custom"); - } - - @Override - public boolean supportsTabs() { - return true; - } - - @Override - public void launch(LaunchConfiguration configuration) throws Exception { - var custom = AppPrefs.get().customTerminalCommand().getValue(); - if (custom == null || custom.isBlank()) { - throw ErrorEvent.expected(new IllegalStateException("No custom terminal command specified")); - } - - var format = custom.toLowerCase(Locale.ROOT).contains("$cmd") ? custom : custom + " $CMD"; - try (var pc = LocalShell.getShell()) { - var toExecute = ExternalApplicationHelper.replaceFileArgument(format, "CMD", configuration.getScriptFile().toString()); - // We can't be sure whether the command is blocking or not, so always make it not blocking - if (pc.getOsType().equals(OsType.WINDOWS)) { - toExecute = "start \"" + configuration.getCleanTitle() + "\" " + toExecute; - } else { - toExecute = "nohup " + toExecute + " /dev/null & disown"; - } - pc.executeSimpleCommand(toExecute); - } - } - - @Override - public boolean isAvailable() { - return true; - } - } - abstract class MacOsType extends ExternalApplicationType.MacApplication implements ExternalTerminalType { public MacOsType(String id, String applicationName) { @@ -776,4 +714,5 @@ public interface ExternalTerminalType extends PrefsChoiceValue { protected abstract CommandBuilder toCommand(LaunchConfiguration configuration) throws Exception; } + } 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 942effeb..08c7d073 100644 --- a/app/src/main/java/io/xpipe/app/terminal/KittyTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/KittyTerminalType.java @@ -9,83 +9,32 @@ import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.store.FilePath; import io.xpipe.core.util.XPipeInstallation; -public class KittyTerminalType { +public interface KittyTerminalType extends ExternalTerminalType { - public static final ExternalTerminalType KITTY_LINUX = new ExternalTerminalType() { + @Override + default boolean supportsColoredTitle() { + return true; + } - @Override - public String getId() { - return "app.kitty"; - } + @Override + default boolean isRecommended() { + return true; + } - @Override - public boolean supportsTabs() { - return true; - } + @Override + default boolean supportsTabs() { + return true; + } - @Override - public void launch(LaunchConfiguration configuration) throws Exception { - try (var sc = LocalShell.getShell().start()) { - CommandSupport.isInPathOrThrow(sc, "kitty", "Kitty", null); - CommandSupport.isInPathOrThrow(sc, "socat", "socat", null); - } + @Override + default String getWebsite() { + return "https://github.com/kovidgoyal/kitty"; + } - var toClose = prepare(); - var socketWrite = CommandBuilder.of().add("socat", "-"); - open(configuration, socketWrite); - if (toClose) { - closeInitial(socketWrite); - } - } - private boolean prepare() throws Exception { - var socket = getSocket(); - try (var sc = LocalShell.getShell().start()) { - if (sc.executeSimpleBooleanCommand("test -w " + sc.getShellDialect().fileArgument(socket))) { - return false; - } + public static final ExternalTerminalType KITTY_LINUX = new Linux(); - sc.executeSimpleCommand(CommandBuilder.of().add("kitty").add("-o", "allow_remote_control=socket-only", "--listen-on", "unix:" + getSocket(), "--detach")); - ThreadHelper.sleep(1500); - return true; - } - } - }; - - public static final ExternalTerminalType KITTY_MACOS = new ExternalTerminalType.MacOsType("app.kitty", "kitty") { - - @Override - public boolean supportsTabs() { - return true; - } - - @Override - public void launch(LaunchConfiguration configuration) throws Exception { - try (var sc = LocalShell.getShell().start()) { - CommandSupport.isInPathOrThrow(sc, "nc", "Netcat", null); - } - - var toClose = prepare(); - var socketWrite = CommandBuilder.of().add("nc", "-U"); - open(configuration, socketWrite); - if (toClose) { - closeInitial(socketWrite); - } - } - - private boolean prepare() throws Exception { - var socket = getSocket(); - try (var sc = LocalShell.getShell().start()) { - if (sc.executeSimpleBooleanCommand("test -w " + sc.getShellDialect().fileArgument(socket))) { - return false; - } - - sc.executeSimpleCommand(CommandBuilder.of().add("open", "-a", "kitty.app", "--args").add("-o", "allow_remote_control=socket-only", "--listen-on", "unix:" + getSocket())); - ThreadHelper.sleep(1000); - return true; - } - } - }; + public static final ExternalTerminalType KITTY_MACOS = new MacOs(); private static FilePath getSocket() throws Exception { try (var sc = LocalShell.getShell().start()) { @@ -132,4 +81,72 @@ public class KittyTerminalType { sc.executeSimpleCommand(CommandBuilder.of().add("echo", "-en", echoString, "|").add(socketWrite).addFile(getSocket())); } } + + class Linux implements KittyTerminalType { + + @Override + public String getId() { + return "app.kitty"; + } + + @Override + public void launch(LaunchConfiguration configuration) throws Exception { + try (var sc = LocalShell.getShell().start()) { + CommandSupport.isInPathOrThrow(sc, "kitty", "Kitty", null); + CommandSupport.isInPathOrThrow(sc, "socat", "socat", null); + } + + var toClose = prepare(); + var socketWrite = CommandBuilder.of().add("socat", "-"); + open(configuration, socketWrite); + if (toClose) { + closeInitial(socketWrite); + } + } + + private boolean prepare() throws Exception { + var socket = getSocket(); + try (var sc = LocalShell.getShell().start()) { + if (sc.executeSimpleBooleanCommand("test -w " + sc.getShellDialect().fileArgument(socket))) { + return false; + } + + sc.executeSimpleCommand(CommandBuilder.of().add("kitty").add("-o", "allow_remote_control=socket-only", "--listen-on", "unix:" + getSocket(), "--detach")); + ThreadHelper.sleep(1500); + return true; + } + } + } + + class MacOs extends MacOsType implements KittyTerminalType { + + public MacOs() {super("app.kitty", "kitty");} + + @Override + public void launch(LaunchConfiguration configuration) throws Exception { + try (var sc = LocalShell.getShell().start()) { + CommandSupport.isInPathOrThrow(sc, "nc", "Netcat", null); + } + + var toClose = prepare(); + var socketWrite = CommandBuilder.of().add("nc", "-U"); + open(configuration, socketWrite); + if (toClose) { + closeInitial(socketWrite); + } + } + + private boolean prepare() throws Exception { + var socket = getSocket(); + try (var sc = LocalShell.getShell().start()) { + if (sc.executeSimpleBooleanCommand("test -w " + sc.getShellDialect().fileArgument(socket))) { + return false; + } + + sc.executeSimpleCommand(CommandBuilder.of().add("open", "-a", "kitty.app", "--args").add("-o", "allow_remote_control=socket-only", "--listen-on", "unix:" + getSocket())); + ThreadHelper.sleep(1000); + return true; + } + } + } } diff --git a/app/src/main/java/io/xpipe/app/terminal/TabbyTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/TabbyTerminalType.java new file mode 100644 index 00000000..0ee04a31 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/terminal/TabbyTerminalType.java @@ -0,0 +1,95 @@ +package io.xpipe.app.terminal; + +import io.xpipe.app.util.LocalShell; +import io.xpipe.app.util.WindowsRegistry; +import io.xpipe.core.process.CommandBuilder; +import io.xpipe.core.process.ShellDialects; + +import java.nio.file.Path; +import java.util.Optional; + +public interface TabbyTerminalType extends ExternalTerminalType { + + static class Windows extends ExternalTerminalType.WindowsType implements TabbyTerminalType { + + public Windows() { + super("app.tabby", "Tabby.exe"); + } + + + @Override + protected void execute(Path file, LaunchConfiguration 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()) + .add("run") + .addFile(configuration.getScriptFile()) + .discardOutput()); + } + + // This is probably not going to work as it does not launch a bat file + LocalShell.getShell().executeSimpleCommand(CommandBuilder.of() + .addFile(file.toString()) + .add("run") + .add(configuration.getDialectLaunchCommand()) + .discardOutput()); + } + + @Override + protected Optional determineInstallation() { + var perUser = WindowsRegistry.readString(WindowsRegistry.HKEY_CURRENT_USER, "SOFTWARE\\71445fac-d6ef-5436-9da7-5a323762d7f5", + "InstallLocation").map(p -> p + "\\Tabby.exe").map(Path::of); + if (perUser.isPresent()) { + return perUser; + } + + var systemWide = WindowsRegistry.readString(WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\71445fac-d6ef-5436-9da7-5a323762d7f5", + "InstallLocation").map(p -> p + "\\Tabby.exe").map(Path::of); + return systemWide; + } + + } + + ExternalTerminalType TABBY_WINDOWS = new Windows(); + + ExternalTerminalType TABBY_MAC_OS = new MacOs(); + + @Override + default String getWebsite() { + return "https://tabby.sh"; + } + + @Override + default boolean isRecommended() { + return true; + } + + @Override + default boolean supportsTabs() { + return true; + } + + @Override + default boolean supportsColoredTitle() { + return true; + } + + class MacOs extends MacOsType implements TabbyTerminalType { + + public MacOs() { + super("app.tabby", "Tabby"); + } + + @Override + public void launch(LaunchConfiguration configuration) throws Exception { + LocalShell.getShell() + .executeSimpleCommand(CommandBuilder.of() + .add("open", "-a") + .addQuoted("Tabby.app") + .add("-n", "--args", "run") + .addFile(configuration.getScriptFile())); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java new file mode 100644 index 00000000..bbf5451d --- /dev/null +++ b/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java @@ -0,0 +1,94 @@ +package io.xpipe.app.terminal; + +import io.xpipe.app.prefs.ExternalApplicationHelper; +import io.xpipe.app.util.LocalShell; +import io.xpipe.app.util.WindowsRegistry; +import io.xpipe.core.process.CommandBuilder; + +import java.nio.file.Path; +import java.util.Optional; + +public interface WezTerminalType extends ExternalTerminalType { + + static class Windows extends WindowsType implements WezTerminalType { + + public Windows() { + super("app.wezterm", "wezterm-gui"); + } + + @Override + protected void execute(Path file, LaunchConfiguration configuration) throws Exception { + LocalShell.getShell().executeSimpleCommand(CommandBuilder.of().addFile(file.toString()).add("start").add(configuration.getDialectLaunchCommand())); + } + + @Override + protected Optional determineInstallation() { + Optional launcherDir; + launcherDir = WindowsRegistry.readString( + WindowsRegistry.HKEY_LOCAL_MACHINE, + "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{BCF6F0DA-5B9A-408D-8562-F680AE6E1EAF}_is1", + "InstallLocation") + .map(p -> p + "\\wezterm-gui.exe"); + return launcherDir.map(Path::of); + } + + } + + static class Linux extends SimplePathType implements WezTerminalType { + + public Linux() { + super("app.wezterm", "wezterm-gui", true); + } + + @Override + protected CommandBuilder toCommand(LaunchConfiguration configuration) { + return CommandBuilder.of().add("start").addFile(configuration.getScriptFile()); + } + + } + + ExternalTerminalType WEZTERM_WINDOWS = new Windows(); + ExternalTerminalType WEZTERM_LINUX = new Linux(); + ExternalTerminalType WEZTERM_MAC_OS = new MacOs(); + + @Override + default String getWebsite() { + return "https://wezfurlong.org/wezterm/index.html"; + } + + @Override + default boolean isRecommended() { + return false; + } + + @Override + default boolean supportsTabs() { + return false; + } + + @Override + default boolean supportsColoredTitle() { + return true; + } + + class MacOs extends MacOsType implements WezTerminalType { + + public MacOs() { + super("app.wezterm", "WezTerm"); + } + + @Override + public void launch(LaunchConfiguration configuration) throws Exception { + var c = CommandBuilder.of() + .addFile(getApplicationPath() + .orElseThrow() + .resolve("Contents") + .resolve("MacOS") + .resolve("wezterm-gui") + .toString()) + .add("start") + .add(configuration.getDialectLaunchCommand()); + ExternalApplicationHelper.startAsync(c); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalType.java index 032a966b..a5ab1fc3 100644 --- a/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/WindowsTerminalType.java @@ -8,41 +8,70 @@ import io.xpipe.core.store.FileNames; import java.nio.file.Files; import java.nio.file.Path; -public class WindowsTerminalType { +public interface WindowsTerminalType extends ExternalTerminalType { - public static final ExternalTerminalType WINDOWS_TERMINAL = - new ExternalTerminalType.SimplePathType("app.windowsTerminal", "wt.exe", false) { + @Override + default boolean isRecommended() { + return true; + } - @Override - protected CommandBuilder toCommand(LaunchConfiguration configuration) throws Exception { - return WindowsTerminalType.toCommand(configuration); - } + @Override + default boolean supportsTabs() { + return true; + } - @Override - public boolean supportsTabs() { - return true; - } + @Override + default boolean supportsColoredTitle() { + return false; + } - @Override - public boolean supportsColoredTitle() { - return false; - } - }; + public static final ExternalTerminalType WINDOWS_TERMINAL = new Standard(); - public static final ExternalTerminalType WINDOWS_TERMINAL_PREVIEW = new ExternalTerminalType() { + public static final ExternalTerminalType WINDOWS_TERMINAL_PREVIEW = new Preview(); + + private static CommandBuilder toCommand(ExternalTerminalType.LaunchConfiguration configuration) throws Exception { + // A weird behavior in Windows Terminal causes the trailing + // backslash of a filepath to escape the closing quote in the title argument + // So just remove that slash + var fixedName = FileNames.removeTrailingSlash(configuration.getColoredTitle()); + + var toExec = !ShellDialects.isPowershell(LocalShell.getShell()) + ? CommandBuilder.of().addFile(configuration.getScriptFile()) + : CommandBuilder.of() + .add("powershell", "-ExecutionPolicy", "Bypass", "-File") + .addFile(configuration.getScriptFile()); + var cmd = CommandBuilder.of().add("-w", "1", "nt"); + + if (configuration.getColor() != null) { + cmd.add("--tabColor").addQuoted(configuration.getColor().toHexString()); + } + return cmd.add("--title").addQuoted(fixedName).add(toExec); + } + + class Standard extends SimplePathType implements WindowsTerminalType { + + public Standard() {super("app.windowsTerminal", "wt.exe", false);} @Override - public boolean supportsTabs() { - return true; + public String getWebsite() { + return "https://aka.ms/terminal-preview"; } @Override - public boolean supportsColoredTitle() { - return false; + protected CommandBuilder toCommand(LaunchConfiguration configuration) throws Exception { + return WindowsTerminalType.toCommand(configuration); + } + } + + class Preview implements WindowsTerminalType { + + @Override + public String getWebsite() { + return "https://aka.ms/terminal"; } @Override - public void launch(ExternalTerminalType.LaunchConfiguration configuration) throws Exception { + public void launch(LaunchConfiguration configuration) throws Exception { LocalShell.getShell() .executeSimpleCommand( CommandBuilder.of().addFile(getPath().toString()).add(toCommand(configuration))); @@ -63,24 +92,5 @@ public class WindowsTerminalType { public String getId() { return "app.windowsTerminalPreview"; } - }; - - private static CommandBuilder toCommand(ExternalTerminalType.LaunchConfiguration configuration) throws Exception { - // A weird behavior in Windows Terminal causes the trailing - // backslash of a filepath to escape the closing quote in the title argument - // So just remove that slash - var fixedName = FileNames.removeTrailingSlash(configuration.getColoredTitle()); - - var toExec = !ShellDialects.isPowershell(LocalShell.getShell()) - ? CommandBuilder.of().addFile(configuration.getScriptFile()) - : CommandBuilder.of() - .add("powershell", "-ExecutionPolicy", "Bypass", "-File") - .addFile(configuration.getScriptFile()); - var cmd = CommandBuilder.of().add("-w", "1", "nt"); - - if (configuration.getColor() != null) { - cmd.add("--tabColor").addQuoted(configuration.getColor().toHexString()); - } - return cmd.add("--title").addQuoted(fixedName).add(toExec); } }