mirror of
https://github.com/xpipe-io/xpipe.git
synced 2026-09-25 09:55:52 +00:00
Terminal rework
This commit is contained in:
@@ -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)));
|
||||
}
|
||||
|
||||
Generated
+6
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user