mirror of
https://github.com/xpipe-io/xpipe.git
synced 2026-04-22 15:40:31 -04:00
Terminal rework
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user