Terminal rework

This commit is contained in:
crschnick
2025-03-26 02:15:26 +00:00
parent 2cf5d98f3f
commit d9c9482b8e
11 changed files with 164 additions and 22 deletions
@@ -313,7 +313,6 @@ public class AppPrefs {
DataStorage.get().forceRewrite();
}
});
INSTANCE.terminalProxy.setValue(UUID.fromString("08438e45-1d9f-4ce6-bbd7-cf47514d15f1"));
}
public static void setLocalDefaultsIfNeeded() {
@@ -1,25 +1,35 @@
package io.xpipe.app.prefs;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.ChoiceComp;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.base.StackComp;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.comp.store.StoreChoiceComp;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.password.PasswordManager;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.terminal.ExternalTerminalType;
import io.xpipe.app.terminal.TerminalLauncher;
import io.xpipe.app.terminal.TerminalMultiplexer;
import io.xpipe.app.terminal.TerminalProxyManager;
import io.xpipe.app.util.*;
import io.xpipe.core.process.OsType;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ListCell;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import org.kordamp.ikonli.javafx.FontIcon;
@@ -79,8 +89,14 @@ public class TerminalCategory extends AppPrefsCategory {
.apply(struc -> struc.get().setPromptText("myterminal -e $CMD"))
.hide(prefs.terminalType.isNotEqualTo(ExternalTerminalType.CUSTOM)))
.addComp(terminalTest)
.pref(prefs.clearTerminalOnInit)
.addToggle(prefs.clearTerminalOnInit))
)
.sub(terminalProxy())
.sub(terminalMultiplexer())
.sub(terminalInitScript())
.sub(new OptionsBuilder()
.pref(prefs.clearTerminalOnInit)
.addToggle(prefs.clearTerminalOnInit)
)
.addTitle("sessionLogging")
.sub(new OptionsBuilder()
.pref(prefs.enableTerminalLogging)
@@ -152,4 +168,64 @@ public class TerminalCategory extends AppPrefsCategory {
struc.get().setSpacing(10);
});
}
private OptionsBuilder terminalProxy() {
var prefs = AppPrefs.get();
var ref = new SimpleObjectProperty<DataStoreEntryRef<ShellStore>>(prefs.terminalProxy().getValue() != null ?
DataStorage.get().getStoreEntryIfPresent(prefs.terminalProxy().getValue()).orElse(DataStorage.get().local()).ref() :
DataStorage.get().local().ref());
ref.addListener((observable, oldValue, newValue) -> {
prefs.terminalProxy.setValue(newValue != null ? newValue.get().getUuid() : null);
});
return new OptionsBuilder()
.nameAndDescription("terminalEnvironment")
.addComp(new StoreChoiceComp<>(StoreChoiceComp.Mode.PROXY, null, ref, ShellStore.class,
r -> TerminalProxyManager.canUseAsProxy(r), StoreViewState.get().getAllConnectionsCategory()), ref)
.hide(OsType.getLocal() != OsType.WINDOWS);
}
private OptionsBuilder terminalInitScript() {
var prefs = AppPrefs.get();
var ref = new SimpleObjectProperty<DataStoreEntryRef<ShellStore>>();
prefs.terminalProxy().subscribe(uuid -> {
ref.set(uuid != null ?
DataStorage.get().getStoreEntryIfPresent(uuid).orElse(DataStorage.get().local()).ref() :
DataStorage.get().local().ref());
});
var script = new SimpleObjectProperty<>(prefs.terminalInitScript().getValue());
return new OptionsBuilder()
.nameAndDescription("terminalInitScript")
.addComp(IntegratedTextAreaComp.script(ref, script).minHeight(150), script);
}
private OptionsBuilder terminalMultiplexer() {
var prefs = AppPrefs.get();
var choiceBuilder = OptionsChoiceBuilder.builder()
.property(prefs.terminalMultiplexer)
.allowNull(true)
.subclasses(TerminalMultiplexer.getClasses())
.transformer(entryComboBox -> {
var websiteLinkButton =
new ButtonComp(AppI18n.observable("website"), new FontIcon("mdi2w-web"), () -> {
var l = prefs.terminalMultiplexer().getValue().getDocsLink();
if (l != null) {
Hyperlinks.open(l);
}
});
websiteLinkButton.minWidth(Region.USE_PREF_SIZE);
websiteLinkButton.disable(Bindings.createBooleanBinding(() -> {
return prefs.terminalMultiplexer.getValue().getDocsLink() == null;
}, prefs.terminalMultiplexer));
var hbox = new HBox(entryComboBox, websiteLinkButton.createRegion());
HBox.setHgrow(entryComboBox, Priority.ALWAYS);
hbox.setSpacing(10);
return hbox;
}).build();
var choice = choiceBuilder.build().buildComp();
return new OptionsBuilder()
.nameAndDescription("terminalMultiplexer")
.addComp(choice);
}
}
@@ -27,13 +27,13 @@ import java.time.format.DateTimeFormatter;
import java.util.UUID;
@Value
@With
public class TerminalLaunchConfiguration {
DataColor color;
String coloredTitle;
String cleanTitle;
boolean preferTabs;
@With
FilePath scriptFile;
ShellDialect scriptDialect;
@@ -1,6 +1,5 @@
package io.xpipe.app.terminal;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.ScriptHelper;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.beacon.BeaconServerException;
@@ -95,7 +94,7 @@ public class TerminalLaunchRequest {
};
try {
var command = TerminalLauncher.launchMultiplexer(processControl, config, wd);
var command = TerminalLauncher.createLaunchCommand(processControl, config, wd);
var file = ScriptHelper.createLocalExecScript(command);
setResult(new TerminalLaunchResult.ResultSuccess(file.asLocalPath()));
} catch (Exception e) {
@@ -129,6 +129,9 @@ public class TerminalLauncher {
var latch = TerminalLauncherManager.submitAsync(request, cc, terminalConfig, directory);
try {
if (!checkMultiplexerLaunch(request, config)) {
if (preferTabs && shouldUseMultiplexer()) {
config = config.withPreferTabs(false).withCleanTitle("XPipe").withColoredTitle("XPipe");
}
type.launch(config);
}
latch.await();
@@ -141,7 +144,25 @@ public class TerminalLauncher {
}
}
private static boolean shouldUseMultiplexer() {
var type = AppPrefs.get().terminalType().getValue();
if (type.getOpenFormat() == TerminalOpenFormat.TABBED) {
return false;
}
var multiplexer = AppPrefs.get().terminalMultiplexer().getValue();
return multiplexer != null;
}
private static boolean checkMultiplexerLaunch(UUID request, TerminalLaunchConfiguration config) throws Exception {
if (!config.isPreferTabs()) {
return false;
}
if (!shouldUseMultiplexer()) {
return false;
}
if (!TerminalMultiplexerManager.requiresNewTerminalSession(request)) {
var control = TerminalProxyManager.getProxy();
if (control.isPresent()) {
@@ -149,7 +170,7 @@ public class TerminalLauncher {
var title = type.useColoredTitle() ? config.getColoredTitle() : config.getCleanTitle();
var openCommand = control.get().prepareTerminalOpen(TerminalInitScriptConfig.ofName(title), WorkingDirectoryFunction.none());
var multiplexer = AppPrefs.get().terminalMultiplexer().getValue();
var fullCommand = multiplexer.launchScriptExternal(openCommand).toString();
var fullCommand = multiplexer.launchScriptExternal(control.get(), openCommand).toString();
control.get().command(fullCommand).execute();
return true;
}
@@ -157,13 +178,13 @@ public class TerminalLauncher {
return false;
}
public static String launchMultiplexer(ProcessControl processControl, TerminalInitScriptConfig config, WorkingDirectoryFunction wd) throws Exception {
public static String createLaunchCommand(ProcessControl processControl, TerminalInitScriptConfig config, WorkingDirectoryFunction wd) throws Exception {
var initScript = AppPrefs.get().terminalInitScript().getValue();
var initialCommand = initScript != null ? initScript.toString() : "";
var openCommand = processControl.prepareTerminalOpen(config, wd);
var proxy = TerminalProxyManager.getProxy();
var multiplexer = AppPrefs.get().terminalMultiplexer().getValue();
var fullCommand = initialCommand + "\n" + (multiplexer != null ? multiplexer.launchScriptSession(openCommand).toString() : openCommand);
var fullCommand = initialCommand + "\n" + (multiplexer != null ? multiplexer.launchScriptSession(proxy.isPresent() ? proxy.get() : LocalShell.getShell(), openCommand).toString() : openCommand);
if (proxy.isPresent()) {
var proxyOpenCommand = fullCommand;
var proxyLaunchCommand = proxy.get().prepareIntermediateTerminalOpen(
@@ -1,6 +1,7 @@
package io.xpipe.app.terminal;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellScript;
import io.xpipe.core.util.ValidationException;
@@ -20,7 +21,7 @@ public interface TerminalMultiplexer {
String getDocsLink();
ShellScript launchScriptExternal(String command) throws Exception;
ShellScript launchScriptExternal(ShellControl control, String command) throws Exception;
ShellScript launchScriptSession(String command) throws Exception;
ShellScript launchScriptSession(ShellControl control, String command) throws Exception;
}
@@ -13,6 +13,7 @@ import io.xpipe.core.process.WorkingDirectoryFunction;
import io.xpipe.core.store.DataStore;
import lombok.Value;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@@ -26,6 +27,28 @@ public class TerminalProxyManager {
private static ActiveSession activeSession;
public static boolean canUseAsProxy(DataStoreEntryRef<ShellStore> ref) {
if (!ref.get().getValidity().isUsable()) {
return false;
}
if (ref.get().equals(DataStorage.get().local())) {
return true;
}
var parent = DataStorage.get().getDefaultDisplayParent(ref.get());
if (parent.isEmpty()) {
return false;
}
if (!parent.get().equals(DataStorage.get().local()) && !DataStorage.get().local().equals(DataStorage.get().getDefaultDisplayParent(parent.get()).orElse(null))) {
return false;
}
var id = ref.get().getProvider().getId();
return List.of("gitWindows", "cygwin", "msys2", "wsl").contains(id);
}
public static Optional<ShellControl> getProxy() {
var uuid = AppPrefs.get().terminalProxy().getValue();
var hasCustomTerminalShell = uuid != null &&
@@ -46,18 +46,24 @@ public class ZellijTerminalMultiplexer implements TerminalMultiplexer {
}
@Override
public ShellScript launchScriptExternal(String command) throws Exception {
public ShellScript launchScriptExternal(ShellControl control, String command) throws Exception {
return ShellScript.lines(
"zellij attach --create-background xpipe",
"zellij run --close-on-exit -- " + command
"zellij -s xpipe action new-tab",
"zellij -s xpipe action write-chars -- " + command.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\\\"")
.replaceAll(" ", "\\\\ "),
"zellij -s xpipe action write 10"
);
}
@Override
public ShellScript launchScriptSession(String command) throws Exception {
public ShellScript launchScriptSession(ShellControl control, String command) throws Exception {
return ShellScript.lines(
"zellij delete-session -f xpipe",
"zellij attach --create-background xpipe",
"zellij run --close-on-exit -- " + command,
"zellij -s xpipe action new-tab",
"zellij -s xpipe action write-chars -- " + command.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\\\"").replaceAll(" ", "\\\\ "),
"zellij -s xpipe action write 10",
"zellij attach xpipe"
);
}
@@ -213,6 +213,10 @@ public class OptionsBuilder {
return this;
}
public OptionsBuilder hide(boolean b) {
return hide(new SimpleBooleanProperty(b));
}
public OptionsBuilder hide(ObservableValue<Boolean> b) {
lastCompHeadReference.hide(b);
return this;
@@ -43,17 +43,21 @@ public class OptionsChoiceBuilder {
private final Property<?> property;
private final List<Class<?>> subclasses;
private final Function<ComboBox<ChoicePaneComp.Entry>, Region> transformer;
private final boolean allowNull;
@SuppressWarnings("unchecked")
public <T> OptionsBuilder build() {
Property<T> s = (Property<T>) property;
var sub = subclasses;
var selectedIndex = s.getValue() == null ? -1 : sub.stream().filter(c -> c.equals(s.getValue().getClass()))
var selectedIndex = s.getValue() == null ? (allowNull ? 0 : -1) : sub.stream().filter(c -> c.equals(s.getValue().getClass()))
.findFirst().map(c -> sub.indexOf(c))
.orElse(0);
.orElse(-1);
var selected = new SimpleIntegerProperty(selectedIndex);
var properties = new ArrayList<Property<Object>>();
if (allowNull) {
properties.add(new SimpleObjectProperty<>());
}
for (int i = 0; i < sub.size(); i++) {
properties.add(new SimpleObjectProperty<>(selectedIndex == i ? s.getValue() : null));
}
@@ -72,6 +76,9 @@ public class OptionsChoiceBuilder {
});
var map = new LinkedHashMap<ObservableValue<String>, OptionsBuilder>();
if (allowNull) {
map.put(AppI18n.observable("none"), new OptionsBuilder());
}
for (int i = 0; i < sub.size(); i++) {
map.put(AppI18n.observable(createIdForClass(sub.get(i))), createOptionsForClass(sub.get(i), properties.get(i)));
}
+6
View File
@@ -1369,3 +1369,9 @@ chooseTemplate=Choose template
keePassXcPlaceholder=KeePassXC entry URL
passwordManagerTestPlaceholder=Enter $NAME$
hide=Hide
terminalEnvironment=Terminal environment
terminalEnvironmentDescription=In case you want to use features of a local Linux-based environment for your terminal customization, e.g. WSL, Cygwin, MSYS2, or Git bash, you can use them as the terminal environment.\n\nAny custom terminal init commands and terminal multiplexer configuration will then be run in this environment.
terminalInitScript=Terminal init script
terminalInitScriptDescription=Commands to run in the terminal environment prior to the connection being launched.\n\nYou can use this to configure the terminal environment with colors, terminal multiplexers, and more on startup.
terminalMultiplexer=Terminal multiplexer
terminalMultiplexerDescription=The terminal multiplexer to use as an alternative to tabs in a terminal.\n\nThis will replace certain terminal handling characteristics, e.g. tab handling, with the multiplexer functionality.