Rework terminal integrations

This commit is contained in:
crschnick 2024-04-02 03:41:08 +00:00
parent 3293f27e6f
commit 91e1a37cdb
9 changed files with 707 additions and 347 deletions

View file

@ -137,10 +137,10 @@ public class AppPrefs {
new AboutCategory(), new AboutCategory(),
new SystemCategory(), new SystemCategory(),
new AppearanceCategory(), new AppearanceCategory(),
new SyncCategory(),
new VaultCategory(),
new TerminalCategory(), new TerminalCategory(),
new EditorCategory(), new EditorCategory(),
new SyncCategory(),
new VaultCategory(),
new LocalShellCategory(), new LocalShellCategory(),
new SecurityCategory(), new SecurityCategory(),
new PasswordManagerCategory(), new PasswordManagerCategory(),

View file

@ -5,9 +5,12 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.impl.ChoiceComp; 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.StackComp;
import io.xpipe.app.fxcomps.impl.TextFieldComp; import io.xpipe.app.fxcomps.impl.TextFieldComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.terminal.ExternalTerminalType; import io.xpipe.app.terminal.ExternalTerminalType;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.OptionsBuilder; import io.xpipe.app.util.OptionsBuilder;
import io.xpipe.app.util.TerminalLauncher; import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
@ -15,6 +18,8 @@ import io.xpipe.core.store.LocalStore;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.ListCell;
import javafx.scene.paint.Color;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List; import java.util.List;
@ -26,6 +31,57 @@ public class TerminalCategory extends AppPrefsCategory {
return "terminal"; 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 @Override
protected Comp<?> create() { protected Comp<?> create() {
var prefs = AppPrefs.get(); var prefs = AppPrefs.get();
@ -46,8 +102,7 @@ public class TerminalCategory extends AppPrefsCategory {
.addTitle("terminalConfiguration") .addTitle("terminalConfiguration")
.sub(new OptionsBuilder() .sub(new OptionsBuilder()
.nameAndDescription("terminalEmulator") .nameAndDescription("terminalEmulator")
.addComp(ChoiceComp.ofTranslatable( .addComp(terminalChoice(), prefs.terminalType)
prefs.terminalType, PrefsChoiceValue.getSupported(ExternalTerminalType.class), false))
.nameAndDescription("customTerminalCommand") .nameAndDescription("customTerminalCommand")
.addComp(new TextFieldComp(prefs.customTerminalCommand, true) .addComp(new TextFieldComp(prefs.customTerminalCommand, true)
.apply(struc -> struc.get().setPromptText("myterminal -e $CMD")) .apply(struc -> struc.get().setPromptText("myterminal -e $CMD"))

View file

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

View file

@ -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 &>/dev/null & disown";
}
pc.executeSimpleCommand(toExecute);
}
}
@Override
public boolean isAvailable() {
return true;
}
}

View file

@ -1,9 +1,6 @@
package io.xpipe.app.terminal; package io.xpipe.app.terminal;
import io.xpipe.app.ext.PrefsChoiceValue; 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.prefs.ExternalApplicationType;
import io.xpipe.app.storage.DataStoreColor; import io.xpipe.app.storage.DataStoreColor;
import io.xpipe.app.util.*; import io.xpipe.app.util.*;
@ -23,6 +20,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
ExternalTerminalType CMD = new SimplePathType("app.cmd", "cmd.exe", true) { ExternalTerminalType CMD = new SimplePathType("app.cmd", "cmd.exe", true) {
@Override
public boolean isRecommended() {
return false;
}
@Override @Override
public boolean supportsTabs() { public boolean supportsTabs() {
return false; return false;
@ -45,6 +47,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
ExternalTerminalType POWERSHELL = new SimplePathType("app.powershell", "powershell", true) { ExternalTerminalType POWERSHELL = new SimplePathType("app.powershell", "powershell", true) {
@Override
public boolean isRecommended() {
return false;
}
@Override @Override
public boolean supportsTabs() { public boolean supportsTabs() {
return false; return false;
@ -80,6 +87,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
ExternalTerminalType PWSH = new SimplePathType("app.pwsh", "pwsh", true) { ExternalTerminalType PWSH = new SimplePathType("app.pwsh", "pwsh", true) {
@Override
public boolean isRecommended() {
return false;
}
@Override @Override
public boolean supportsTabs() { public boolean supportsTabs() {
return false; return false;
@ -104,121 +116,17 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
}); });
} }
}; };
ExternalTerminalType GNOME_TERMINAL = new PathCheckType("app.gnomeTerminal", "gnome-terminal", true) {
ExternalTerminalType ALACRITTY_WINDOWS = new SimplePathType("app.alacritty", "alacritty", false) {
@Override
public boolean supportsTabs() {
return false;
}
@Override @Override
public boolean supportsColoredTitle() { 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; return true;
} }
@Override @Override
protected void execute(Path file, LaunchConfiguration configuration) throws Exception { public boolean isRecommended() {
// 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<Path> 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() {
return false; 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<Path> determineInstallation() {
Optional<String> 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 @Override
public boolean supportsTabs() { public boolean supportsTabs() {
return false; return false;
@ -243,6 +151,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
}; };
ExternalTerminalType KONSOLE = new SimplePathType("app.konsole", "konsole", true) { ExternalTerminalType KONSOLE = new SimplePathType("app.konsole", "konsole", true) {
@Override
public boolean isRecommended() {
return true;
}
@Override @Override
public boolean supportsTabs() { public boolean supportsTabs() {
return true; return true;
@ -262,6 +175,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
} }
}; };
ExternalTerminalType XFCE = new SimplePathType("app.xfce", "xfce4-terminal", true) { ExternalTerminalType XFCE = new SimplePathType("app.xfce", "xfce4-terminal", true) {
@Override
public boolean supportsColoredTitle() {
return true;
}
@Override
public boolean isRecommended() {
return true;
}
@Override @Override
public boolean supportsTabs() { public boolean supportsTabs() {
@ -278,6 +201,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
} }
}; };
ExternalTerminalType ELEMENTARY = new SimplePathType("app.elementaryTerminal", "io.elementary.terminal", true) { ExternalTerminalType ELEMENTARY = new SimplePathType("app.elementaryTerminal", "io.elementary.terminal", true) {
@Override
public boolean supportsColoredTitle() {
return true;
}
@Override
public boolean isRecommended() {
return true;
}
@Override @Override
public boolean supportsTabs() { public boolean supportsTabs() {
@ -290,6 +223,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
} }
}; };
ExternalTerminalType TILIX = new SimplePathType("app.tilix", "tilix", true) { ExternalTerminalType TILIX = new SimplePathType("app.tilix", "tilix", true) {
@Override
public boolean supportsColoredTitle() {
return true;
}
@Override
public boolean isRecommended() {
return false;
}
@Override @Override
public boolean supportsTabs() { public boolean supportsTabs() {
@ -306,6 +249,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
} }
}; };
ExternalTerminalType TERMINATOR = new SimplePathType("app.terminator", "terminator", true) { ExternalTerminalType TERMINATOR = new SimplePathType("app.terminator", "terminator", true) {
@Override
public boolean supportsColoredTitle() {
return true;
}
@Override
public boolean isRecommended() {
return true;
}
@Override @Override
public boolean supportsTabs() { public boolean supportsTabs() {
@ -323,6 +276,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
} }
}; };
ExternalTerminalType TERMINOLOGY = new SimplePathType("app.terminology", "terminology", true) { ExternalTerminalType TERMINOLOGY = new SimplePathType("app.terminology", "terminology", true) {
@Override
public boolean supportsColoredTitle() {
return true;
}
@Override
public boolean isRecommended() {
return true;
}
@Override @Override
public boolean supportsTabs() { public boolean supportsTabs() {
@ -340,6 +303,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
} }
}; };
ExternalTerminalType COOL_RETRO_TERM = new SimplePathType("app.coolRetroTerm", "cool-retro-term", true) { 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 @Override
public boolean supportsTabs() { public boolean supportsTabs() {
@ -356,6 +329,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
} }
}; };
ExternalTerminalType GUAKE = new SimplePathType("app.guake", "guake", true) { ExternalTerminalType GUAKE = new SimplePathType("app.guake", "guake", true) {
@Override
public boolean supportsColoredTitle() {
return true;
}
@Override
public boolean isRecommended() {
return true;
}
@Override @Override
public boolean supportsTabs() { public boolean supportsTabs() {
@ -372,28 +355,17 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
.addFile(configuration.getScriptFile()); .addFile(configuration.getScriptFile());
} }
}; };
ExternalTerminalType ALACRITTY_LINUX = new SimplePathType("app.alacritty", "alacritty", true) { ExternalTerminalType TILDA = new SimplePathType("app.tilda", "tilda", true) {
@Override
public boolean supportsTabs() {
return false;
}
@Override @Override
public boolean supportsColoredTitle() { public boolean supportsColoredTitle() {
return false; return true;
} }
@Override @Override
protected CommandBuilder toCommand(LaunchConfiguration configuration) { public boolean isRecommended() {
return CommandBuilder.of() return true;
.add("-t")
.addQuoted(configuration.getCleanTitle())
.add("-e")
.addFile(configuration.getScriptFile());
} }
};
ExternalTerminalType TILDA = new SimplePathType("app.tilda", "tilda", true) {
@Override @Override
public boolean supportsTabs() { public boolean supportsTabs() {
@ -406,6 +378,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
} }
}; };
ExternalTerminalType XTERM = new SimplePathType("app.xterm", "xterm", true) { ExternalTerminalType XTERM = new SimplePathType("app.xterm", "xterm", true) {
@Override
public boolean supportsColoredTitle() {
return true;
}
@Override
public boolean isRecommended() {
return false;
}
@Override @Override
public boolean supportsTabs() { public boolean supportsTabs() {
@ -422,6 +404,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
} }
}; };
ExternalTerminalType DEEPIN_TERMINAL = new SimplePathType("app.deepinTerminal", "deepin-terminal", true) { ExternalTerminalType DEEPIN_TERMINAL = new SimplePathType("app.deepinTerminal", "deepin-terminal", true) {
@Override
public boolean supportsColoredTitle() {
return true;
}
@Override
public boolean isRecommended() {
return false;
}
@Override @Override
public boolean supportsTabs() { public boolean supportsTabs() {
@ -434,6 +426,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
} }
}; };
ExternalTerminalType Q_TERMINAL = new SimplePathType("app.qTerminal", "qterminal", true) { ExternalTerminalType Q_TERMINAL = new SimplePathType("app.qTerminal", "qterminal", true) {
@Override
public boolean supportsColoredTitle() {
return true;
}
@Override
public boolean isRecommended() {
return false;
}
@Override @Override
public boolean supportsTabs() { public boolean supportsTabs() {
@ -446,6 +448,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
} }
}; };
ExternalTerminalType MACOS_TERMINAL = new MacOsType("app.macosTerminal", "Terminal") { ExternalTerminalType MACOS_TERMINAL = new MacOsType("app.macosTerminal", "Terminal") {
@Override
public boolean supportsColoredTitle() {
return true;
}
@Override
public boolean isRecommended() {
return false;
}
@Override @Override
public boolean supportsTabs() { public boolean supportsTabs() {
return false; return false;
@ -467,6 +479,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
} }
}; };
ExternalTerminalType ITERM2 = new MacOsType("app.iterm2", "iTerm") { ExternalTerminalType ITERM2 = new MacOsType("app.iterm2", "iTerm") {
@Override
public boolean supportsColoredTitle() {
return true;
}
@Override
public boolean isRecommended() {
return true;
}
@Override @Override
public boolean supportsTabs() { public boolean supportsTabs() {
return true; return true;
@ -504,6 +526,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
}; };
ExternalTerminalType WARP = new MacOsType("app.warp", "Warp") { ExternalTerminalType WARP = new MacOsType("app.warp", "Warp") {
@Override
public boolean supportsColoredTitle() {
return true;
}
@Override
public boolean isRecommended() {
return true;
}
@Override @Override
public boolean supportsTabs() { public boolean supportsTabs() {
return true; return true;
@ -523,79 +555,18 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
.addFile(configuration.getScriptFile())); .addFile(configuration.getScriptFile()));
} }
}; };
ExternalTerminalType TABBY_MAC_OS = new MacOsType("app.tabby", "Tabby") { ExternalTerminalType CUSTOM = new CustomTerminalType();
@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();
List<ExternalTerminalType> WINDOWS_TERMINALS = List.of( List<ExternalTerminalType> WINDOWS_TERMINALS = List.of(
TABBY_WINDOWS, TabbyTerminalType.TABBY_WINDOWS,
ALACRITTY_WINDOWS, AlacrittyTerminalType.ALACRITTY_WINDOWS,
WEZ_WINDOWS, WezTerminalType.WEZTERM_WINDOWS,
WindowsTerminalType.WINDOWS_TERMINAL_PREVIEW, WindowsTerminalType.WINDOWS_TERMINAL_PREVIEW,
WindowsTerminalType.WINDOWS_TERMINAL, WindowsTerminalType.WINDOWS_TERMINAL,
CMD, CMD,
PWSH, PWSH,
POWERSHELL); POWERSHELL);
List<ExternalTerminalType> LINUX_TERMINALS = List.of( List<ExternalTerminalType> LINUX_TERMINALS = List.of(
WEZ_LINUX, WezTerminalType.WEZTERM_LINUX,
KONSOLE, KONSOLE,
XFCE, XFCE,
ELEMENTARY, ELEMENTARY,
@ -606,13 +577,13 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
TERMINOLOGY, TERMINOLOGY,
COOL_RETRO_TERM, COOL_RETRO_TERM,
GUAKE, GUAKE,
ALACRITTY_LINUX, AlacrittyTerminalType.ALACRITTY_LINUX,
TILDA, TILDA,
XTERM, XTERM,
DEEPIN_TERMINAL, DEEPIN_TERMINAL,
Q_TERMINAL); Q_TERMINAL);
List<ExternalTerminalType> MACOS_TERMINALS = List<ExternalTerminalType> 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") @SuppressWarnings("TrivialFunctionalExpressionUsage")
List<ExternalTerminalType> ALL = ((Supplier<List<ExternalTerminalType>>) () -> { List<ExternalTerminalType> ALL = ((Supplier<List<ExternalTerminalType>>) () -> {
@ -653,10 +624,14 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
boolean supportsTabs(); boolean supportsTabs();
default boolean supportsColoredTitle() { default String getWebsite() {
return true; return null;
} }
boolean isRecommended();
boolean supportsColoredTitle();
default boolean shouldClear() { default boolean shouldClear() {
return true; 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 &>/dev/null & disown";
}
pc.executeSimpleCommand(toExecute);
}
}
@Override
public boolean isAvailable() {
return true;
}
}
abstract class MacOsType extends ExternalApplicationType.MacApplication implements ExternalTerminalType { abstract class MacOsType extends ExternalApplicationType.MacApplication implements ExternalTerminalType {
public MacOsType(String id, String applicationName) { public MacOsType(String id, String applicationName) {
@ -776,4 +714,5 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
protected abstract CommandBuilder toCommand(LaunchConfiguration configuration) throws Exception; protected abstract CommandBuilder toCommand(LaunchConfiguration configuration) throws Exception;
} }
} }

View file

@ -9,83 +9,32 @@ import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.store.FilePath; import io.xpipe.core.store.FilePath;
import io.xpipe.core.util.XPipeInstallation; 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 @Override
public String getId() { default boolean isRecommended() {
return "app.kitty"; return true;
} }
@Override @Override
public boolean supportsTabs() { default boolean supportsTabs() {
return true; return true;
} }
@Override @Override
public void launch(LaunchConfiguration configuration) throws Exception { default String getWebsite() {
try (var sc = LocalShell.getShell().start()) { return "https://github.com/kovidgoyal/kitty";
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 { public static final ExternalTerminalType KITTY_LINUX = new Linux();
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")); public static final ExternalTerminalType KITTY_MACOS = new MacOs();
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;
}
}
};
private static FilePath getSocket() throws Exception { private static FilePath getSocket() throws Exception {
try (var sc = LocalShell.getShell().start()) { 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())); 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;
}
}
}
} }

View file

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

View file

@ -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<Path> determineInstallation() {
Optional<String> 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);
}
}
}

View file

@ -8,41 +8,70 @@ import io.xpipe.core.store.FileNames;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
public class WindowsTerminalType { public interface WindowsTerminalType extends ExternalTerminalType {
public static final ExternalTerminalType WINDOWS_TERMINAL = @Override
new ExternalTerminalType.SimplePathType("app.windowsTerminal", "wt.exe", false) { default boolean isRecommended() {
return true;
}
@Override @Override
protected CommandBuilder toCommand(LaunchConfiguration configuration) throws Exception { default boolean supportsTabs() {
return WindowsTerminalType.toCommand(configuration); return true;
} }
@Override @Override
public boolean supportsTabs() { default boolean supportsColoredTitle() {
return true; return false;
} }
@Override public static final ExternalTerminalType WINDOWS_TERMINAL = new Standard();
public boolean supportsColoredTitle() {
return false;
}
};
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 @Override
public boolean supportsTabs() { public String getWebsite() {
return true; return "https://aka.ms/terminal-preview";
} }
@Override @Override
public boolean supportsColoredTitle() { protected CommandBuilder toCommand(LaunchConfiguration configuration) throws Exception {
return false; return WindowsTerminalType.toCommand(configuration);
}
}
class Preview implements WindowsTerminalType {
@Override
public String getWebsite() {
return "https://aka.ms/terminal";
} }
@Override @Override
public void launch(ExternalTerminalType.LaunchConfiguration configuration) throws Exception { public void launch(LaunchConfiguration configuration) throws Exception {
LocalShell.getShell() LocalShell.getShell()
.executeSimpleCommand( .executeSimpleCommand(
CommandBuilder.of().addFile(getPath().toString()).add(toCommand(configuration))); CommandBuilder.of().addFile(getPath().toString()).add(toCommand(configuration)));
@ -63,24 +92,5 @@ public class WindowsTerminalType {
public String getId() { public String getId() {
return "app.windowsTerminalPreview"; 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);
} }
} }