Terminal rework

This commit is contained in:
crschnick
2025-03-25 22:04:12 +00:00
parent aae64fe5f7
commit 2cf5d98f3f
10 changed files with 284 additions and 9 deletions

View File

@@ -5,13 +5,17 @@ import io.xpipe.app.core.*;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.ext.PrefsHandler;
import io.xpipe.app.ext.PrefsProvider;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.icon.SystemIconSource;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.password.NoPasswordManager;
import io.xpipe.app.password.PasswordManager;
import io.xpipe.app.password.PasswordManagerCommand;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.terminal.ExternalTerminalType;
import io.xpipe.app.terminal.TerminalMultiplexer;
import io.xpipe.app.terminal.ZellijTerminalMultiplexer;
import io.xpipe.app.util.PlatformState;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.core.process.ShellScript;
@@ -107,6 +111,24 @@ public class AppPrefs {
"passwordManager",
PasswordManager.class,
false);
final Property<ShellScript> terminalInitScript = mapLocal(
new SimpleObjectProperty<>(null),
"terminalInitScript",
ShellScript.class,
false);
final Property<UUID> terminalProxy = mapLocal(
new SimpleObjectProperty<>(),
"terminalProxy",
UUID.class,
false);
final Property<TerminalMultiplexer> terminalMultiplexer = mapLocal(
new SimpleObjectProperty<>(ZellijTerminalMultiplexer.builder().build()),
"terminalMultiplexer",
TerminalMultiplexer.class,
false);
public ObservableValue<UUID> terminalProxy() {
return terminalProxy;
}
public final StringProperty passwordManagerCommand =
mapLocal(new SimpleStringProperty(null), "passwordManagerCommand", String.class, false);
final ObjectProperty<StartupBehaviour> startupBehaviour = mapLocal(
@@ -291,6 +313,7 @@ public class AppPrefs {
DataStorage.get().forceRewrite();
}
});
INSTANCE.terminalProxy.setValue(UUID.fromString("08438e45-1d9f-4ce6-bbd7-cf47514d15f1"));
}
public static void setLocalDefaultsIfNeeded() {
@@ -317,6 +340,14 @@ public class AppPrefs {
return passwordManager;
}
public ObservableValue<TerminalMultiplexer> terminalMultiplexer() {
return terminalMultiplexer;
}
public ObservableValue<ShellScript> terminalInitScript() {
return terminalInitScript;
}
public ObservableValue<SupportedLocale> language() {
return language;
}

View File

@@ -1,5 +1,6 @@
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;
@@ -94,8 +95,9 @@ public class TerminalLaunchRequest {
};
try {
var file = ScriptHelper.createLocalExecScript(processControl.prepareTerminalOpen(config, wd));
setResult(new TerminalLaunchResult.ResultSuccess(Path.of(file.toString())));
var command = TerminalLauncher.launchMultiplexer(processControl, config, wd);
var file = ScriptHelper.createLocalExecScript(command);
setResult(new TerminalLaunchResult.ResultSuccess(file.asLocalPath()));
} catch (Exception e) {
setResult(new TerminalLaunchResult.ResultFailure(e));
}

View File

@@ -128,7 +128,9 @@ public class TerminalLauncher {
var config = TerminalLaunchConfiguration.create(request, entry, cleanTitle, adjustedTitle, preferTabs);
var latch = TerminalLauncherManager.submitAsync(request, cc, terminalConfig, directory);
try {
type.launch(config);
if (!checkMultiplexerLaunch(request, config)) {
type.launch(config);
}
latch.await();
} catch (Exception ex) {
var modMsg = ex.getMessage() != null && ex.getMessage().contains("Unable to find application named")
@@ -138,4 +140,39 @@ public class TerminalLauncher {
"Unable to launch terminal " + type.toTranslatedString().getValue() + ": " + modMsg, ex));
}
}
private static boolean checkMultiplexerLaunch(UUID request, TerminalLaunchConfiguration config) throws Exception {
if (!TerminalMultiplexerManager.requiresNewTerminalSession(request)) {
var control = TerminalProxyManager.getProxy();
if (control.isPresent()) {
var type = AppPrefs.get().terminalType().getValue();
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();
control.get().command(fullCommand).execute();
return true;
}
}
return false;
}
public static String launchMultiplexer(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);
if (proxy.isPresent()) {
var proxyOpenCommand = fullCommand;
var proxyLaunchCommand = proxy.get().prepareIntermediateTerminalOpen(
TerminalInitFunction.fixed(proxyOpenCommand),
TerminalInitScriptConfig.ofName("XPipe"),
WorkingDirectoryFunction.none());
return proxyLaunchCommand;
} else {
return fullCommand;
}
}
}

View File

@@ -0,0 +1,26 @@
package io.xpipe.app.terminal;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.xpipe.core.process.ShellScript;
import io.xpipe.core.util.ValidationException;
import java.util.ArrayList;
import java.util.List;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
public interface TerminalMultiplexer {
static List<Class<?>> getClasses() {
var l = new ArrayList<Class<?>>();
l.add(ZellijTerminalMultiplexer.class);
return l;
}
default void checkComplete() throws ValidationException {}
String getDocsLink();
ShellScript launchScriptExternal(String command) throws Exception;
ShellScript launchScriptSession(String command) throws Exception;
}

View File

@@ -0,0 +1,22 @@
package io.xpipe.app.terminal;
import io.xpipe.app.prefs.AppPrefs;
import java.util.*;
public class TerminalMultiplexerManager {
private static final Set<UUID> connectionHubRequests = new HashSet<>();
public static boolean requiresNewTerminalSession(UUID requestUuid) {
if (AppPrefs.get().terminalMultiplexer().getValue() == null) {
connectionHubRequests.add(requestUuid);
return true;
}
var hasTerminal = TerminalView.get().getSessions().stream().anyMatch(shellSession ->
shellSession.getTerminal().isRunning() && connectionHubRequests.contains(shellSession.getRequest()));
connectionHubRequests.add(requestUuid);
return !hasTerminal;
}
}

View File

@@ -0,0 +1,81 @@
package io.xpipe.app.terminal;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.ScriptHelper;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.TerminalInitFunction;
import io.xpipe.core.process.TerminalInitScriptConfig;
import io.xpipe.core.process.WorkingDirectoryFunction;
import io.xpipe.core.store.DataStore;
import lombok.Value;
import java.util.Optional;
import java.util.UUID;
public class TerminalProxyManager {
@Value
private static class ActiveSession {
UUID uuid;
ShellControl control;
}
private static ActiveSession activeSession;
public static Optional<ShellControl> getProxy() {
var uuid = AppPrefs.get().terminalProxy().getValue();
var hasCustomTerminalShell = uuid != null &&
!DataStorage.get().local().getUuid().equals(uuid);
if (!hasCustomTerminalShell) {
return Optional.empty();
}
var matchingSession = activeSession != null && activeSession.uuid.equals(uuid) ? activeSession : null;
if (matchingSession != null) {
// Probably incompatible
if (matchingSession.control == null) {
return Optional.empty();
}
try {
matchingSession.getControl().start();
return Optional.of(matchingSession.getControl());
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
activeSession = new ActiveSession(uuid, null);
return Optional.empty();
}
}
DataStoreEntryRef<DataStore> ref = DataStorage.get().getStoreEntry(uuid).ref();
try {
var control = createControl(ref);
if (control.isPresent()) {
control.get().start();
activeSession = new ActiveSession(uuid, control.get());
return control;
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
}
activeSession = new ActiveSession(uuid, null);
return Optional.empty();
}
private static Optional<ShellControl> createControl(DataStoreEntryRef<DataStore> ref) throws Exception {
if (ref == null || !ref.get().getValidity().isUsable() || !(ref.getStore() instanceof ShellStore ss)) {
return Optional.empty();
}
var store = ss;
var control = store.standaloneControl();
if (!control.getLocalSystemAccess().supportsExecutables() || !control.getLocalSystemAccess().supportsFileSystemAccess()) {
return Optional.empty();
}
return Optional.of(control);
}
}

View File

@@ -25,9 +25,9 @@ public interface WarpTerminalType extends ExternalTerminalType, TrackableTermina
@Override
public void launch(TerminalLaunchConfiguration configuration) throws Exception {
try (var sc = LocalShell.getShell().start()) {
var command = sc.getShellDialect().getSetEnvironmentVariableCommand("PSModulePath", "") + "\n" +
sc.getShellDialect().runScriptCommand(sc, configuration.getScriptFile().toString());
var script = ScriptHelper.createExecScript(sc, command);
var command = configuration.getScriptDialect().getSetEnvironmentVariableCommand("PSModulePath", "") + "\n" +
configuration.getScriptDialect().runScriptCommand(sc, configuration.getScriptFile().toString());
var script = ScriptHelper.createExecScript(configuration.getScriptDialect(), sc, command);
if (!configuration.isPreferTabs()) {
DesktopHelper.openUrl("warp://action/new_window?path=" + script);
} else {

View File

@@ -0,0 +1,64 @@
package io.xpipe.app.terminal;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.password.KeePassXcAssociationKey;
import io.xpipe.app.password.KeePassXcManager;
import io.xpipe.app.password.KeePassXcProxyClient;
import io.xpipe.app.password.PasswordManager;
import io.xpipe.app.util.LocalShell;
import io.xpipe.app.util.OptionsBuilder;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellScript;
import io.xpipe.core.store.FilePath;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
import lombok.extern.jackson.Jacksonized;
@Getter
@Builder(toBuilder = true)
@ToString
@Jacksonized
@JsonTypeName("zellij")
public class ZellijTerminalMultiplexer implements TerminalMultiplexer {
private final String wslDistribution;
private final FilePath config;
public static OptionsBuilder createOptions(Property<ZellijTerminalMultiplexer> p) {
var config = new SimpleObjectProperty<FilePath>(p.getValue() != null ? p.getValue().getConfig() : null);
return new OptionsBuilder()
.addProperty(config)
.bind(() -> {
return null; //new ZellijTerminalMultiplexer(config.getValue());
}, p);
}
@Override
public String getDocsLink() {
return "";
}
@Override
public ShellScript launchScriptExternal(String command) throws Exception {
return ShellScript.lines(
"zellij attach --create-background xpipe",
"zellij run --close-on-exit -- " + command
);
}
@Override
public ShellScript launchScriptSession(String command) throws Exception {
return ShellScript.lines(
"zellij attach --create-background xpipe",
"zellij run --close-on-exit -- " + command,
"zellij attach xpipe"
);
}
}

View File

@@ -4,6 +4,7 @@ import io.xpipe.app.ext.LocalStore;
import io.xpipe.app.password.PasswordManager;
import io.xpipe.app.storage.*;
import io.xpipe.app.terminal.ExternalTerminalType;
import io.xpipe.app.terminal.TerminalMultiplexer;
import io.xpipe.core.util.InPlaceSecretValue;
import io.xpipe.core.util.JacksonMapper;
@@ -42,9 +43,8 @@ public class AppJacksonModule extends SimpleModule {
addSerializer(EncryptedValue.VaultKey.class, new EncryptedValueSerializer());
addDeserializer(EncryptedValue.VaultKey.class, new EncryptedValueDeserializer<>());
for (Class<?> c : PasswordManager.getClasses()) {
context.registerSubtypes(c);
}
context.registerSubtypes(PasswordManager.getClasses());
context.registerSubtypes(TerminalMultiplexer.getClasses());
context.addSerializers(_serializers);
context.addDeserializers(_deserializers);

View File

@@ -2,8 +2,20 @@ package io.xpipe.core.process;
import lombok.Value;
import java.util.Arrays;
import java.util.stream.Collectors;
@Value
public class ShellScript {
public static ShellScript lines(String... lines) {
return new ShellScript(Arrays.stream(lines).collect(Collectors.joining("\n")));
}
String value;
@Override
public String toString() {
return value;
}
}