diff --git a/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java b/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java index 16b9f17e..15c8e667 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java @@ -6,7 +6,6 @@ import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.core.process.ShellStoreState; -import javafx.beans.binding.Bindings; import javafx.beans.value.ObservableValue; import javafx.geometry.Pos; import javafx.scene.layout.Region; @@ -25,16 +24,14 @@ public class SystemStateComp extends SimpleComp { @Override protected Region createSimple() { - var icon = PlatformThread.sync(Bindings.createStringBinding( - () -> { - return state.getValue() == State.FAILURE - ? "mdi2l-lightning-bolt" - : state.getValue() == State.SUCCESS ? "mdal-check" : "mdsmz-remove"; - }, - state)); var fi = new FontIcon(); fi.getStyleClass().add("inner-icon"); - icon.subscribe(val -> fi.setIconLiteral(val)); + state.subscribe(s -> { + var i = s == State.FAILURE + ? "mdi2l-lightning-bolt" + : s == State.SUCCESS ? "mdal-check" : "mdsmz-remove"; + PlatformThread.runLaterIfNeeded(() -> fi.setIconLiteral(i)); + }); var border = new FontIcon("mdi2c-circle-outline"); border.getStyleClass().add("outer-icon"); diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java index e9d0a86d..4b28606f 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationMenu.java @@ -27,7 +27,7 @@ public class StoreCreationMenu { menu.getItems().add(category("addHost", "mdi2h-home-plus", DataStoreProvider.CreationCategory.HOST, "ssh")); menu.getItems() - .add(category("addVisual", "mdi2c-camera-plus", DataStoreProvider.CreationCategory.VISUAL, null)); + .add(category("addDesktop", "mdi2c-camera-plus", DataStoreProvider.CreationCategory.DESKSTOP, null)); menu.getItems() .add(category("addShell", "mdi2t-text-box-multiple", DataStoreProvider.CreationCategory.SHELL, null)); diff --git a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java index 42ef90e0..2e3a9b4c 100644 --- a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java @@ -24,7 +24,6 @@ import javafx.beans.value.ObservableValue; import java.util.List; public interface DataStoreProvider { - default boolean editByDefault() { return false; } @@ -208,6 +207,6 @@ public interface DataStoreProvider { TUNNEL, SCRIPT, CLUSTER, - VISUAL; + DESKSTOP; } } diff --git a/app/src/main/java/io/xpipe/app/ext/SingletonToggleSessionStoreProvider.java b/app/src/main/java/io/xpipe/app/ext/SingletonToggleSessionStoreProvider.java new file mode 100644 index 00000000..b9fc6f67 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/ext/SingletonToggleSessionStoreProvider.java @@ -0,0 +1,71 @@ +package io.xpipe.app.ext; + +import io.xpipe.app.comp.base.StoreToggleComp; +import io.xpipe.app.comp.base.SystemStateComp; +import io.xpipe.app.comp.store.StoreEntryComp; +import io.xpipe.app.comp.store.StoreEntryWrapper; +import io.xpipe.app.comp.store.StoreSection; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.core.store.SingletonToggleSessionStore; +import io.xpipe.core.store.ToggleSessionState; +import javafx.beans.binding.Bindings; +import javafx.beans.property.SimpleBooleanProperty; + +public interface SingletonToggleSessionStoreProvider extends DataStoreProvider { + + @Override + public default StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) { + SingletonToggleSessionStore s = sec.getWrapper().getEntry().getStore().asNeeded(); + + var enabled = new SimpleBooleanProperty(); + sec.getWrapper().getPersistentState().subscribe((newValue) -> { + var rdps = (ToggleSessionState) newValue; + enabled.set(rdps.getEnabled() != null ? rdps.getEnabled() : false); + }); + + var t = new StoreToggleComp(null, sec, enabled, aBoolean -> { + var state = s.getState(); + if (state.getEnabled() != aBoolean) { + state.setEnabled(aBoolean); + s.setState(state); + sec.getWrapper().getEntry().validate(); + } + }); + return StoreEntryComp.create(sec.getWrapper(), t, preferLarge); + } + + public default Comp stateDisplay(StoreEntryWrapper w) { + SingletonToggleSessionStore st = w.getEntry().getStore().asNeeded(); + return new SystemStateComp( + Bindings.createObjectBinding( + () -> { + ToggleSessionState s = (ToggleSessionState) w.getPersistentState().getValue(); + if (s.getEnabled() == null || !s.getEnabled()) { + return SystemStateComp.State.OTHER; + } + + return s.getRunning() != null && s.getRunning() + ? SystemStateComp.State.SUCCESS + : SystemStateComp.State.FAILURE; + }, + w.getPersistentState(), + w.getCache())); + } + + @Override + public default void storageInit() { + for (DataStoreEntry e : DataStorage.get().getStoreEntries()) { + if (getStoreClasses().stream() + .anyMatch(aClass -> + e.getStore() != null && e.getStore().getClass().equals(aClass))) { + SingletonToggleSessionStore tunnelStore = e.getStore().asNeeded(); + var state = tunnelStore.getState(); + state.setEnabled(false); + tunnelStore.setState(state); + } + } + } + +} diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoiceComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoiceComp.java index 2a56182c..3fa88469 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoiceComp.java @@ -51,6 +51,7 @@ public class ChoiceComp extends Comp>> { @Override public CompStructure> createBase() { var cb = new ComboBox(); + cb.setMaxWidth(2000); cb.setConverter(new StringConverter<>() { @Override public String toString(T object) { diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java index 42db2800..def490d4 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java @@ -34,7 +34,7 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue { return getId(); } - public static class MacApplication extends ExternalApplicationType { + public static abstract class MacApplication extends ExternalApplicationType { protected final String applicationName; diff --git a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java index eb32ae21..2989e401 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java @@ -431,6 +431,10 @@ public class DataStoreEntry extends StorageElement { } public void validateOrThrow() throws Throwable { + if (store == null) { + return; + } + try { store.checkComplete(); setInRefresh(true); diff --git a/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java index 6af87c6f..84d0296c 100644 --- a/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java @@ -7,6 +7,7 @@ import io.xpipe.app.util.CommandSupport; import io.xpipe.app.util.LocalShell; import io.xpipe.core.process.*; import io.xpipe.core.store.FilePath; +import io.xpipe.core.util.FailableFunction; import lombok.Getter; import lombok.Value; import lombok.With; @@ -18,7 +19,6 @@ import java.util.ArrayList; import java.util.Base64; import java.util.Comparator; import java.util.List; -import java.util.function.Supplier; public interface ExternalTerminalType extends PrefsChoiceValue { @@ -152,6 +152,18 @@ public interface ExternalTerminalType extends PrefsChoiceValue { .envrironment("GNOME_TERMINAL_SCREEN", sc -> ""); } } + + @Override + public FailableFunction remoteLaunchCommand() { + return launchConfiguration -> { + var toExecute = CommandBuilder.of() + .add(executable, "-v", "--title") + .addQuoted(launchConfiguration.getColoredTitle()) + .add("--") + .addFile(launchConfiguration.getScriptFile()); + return toExecute.buildSimple(); + }; + } }; ExternalTerminalType KONSOLE = new SimplePathType("app.konsole", "konsole", true) { @@ -551,6 +563,17 @@ public interface ExternalTerminalType extends PrefsChoiceValue { .addQuoted("Warp.app") .addFile(configuration.getScriptFile())); } + + @Override + public FailableFunction remoteLaunchCommand() { + return launchConfiguration -> { + var toExecute = CommandBuilder.of() + .add("open", "-a") + .addQuoted("Warp.app") + .addFile(launchConfiguration.getScriptFile()); + return toExecute.buildSimple(); + }; + } }; ExternalTerminalType CUSTOM = new CustomTerminalType(); List WINDOWS_TERMINALS = List.of( @@ -588,24 +611,31 @@ public interface ExternalTerminalType extends PrefsChoiceValue { WezTerminalType.WEZTERM_MAC_OS, MACOS_TERMINAL); - @SuppressWarnings("TrivialFunctionalExpressionUsage") - List ALL = ((Supplier>) () -> { - var all = new ArrayList(); - if (OsType.getLocal().equals(OsType.WINDOWS)) { - all.addAll(WINDOWS_TERMINALS); - } - if (OsType.getLocal().equals(OsType.LINUX)) { - all.addAll(LINUX_TERMINALS); - } - if (OsType.getLocal().equals(OsType.MACOS)) { - all.addAll(MACOS_TERMINALS); - } - // Prefer with tabs - all.sort(Comparator.comparingInt(o -> (o.supportsTabs() ? -1 : 0))); - all.add(CUSTOM); - return all; - }) - .get(); + List APPLICABLE = getTypes(OsType.getLocal(), false, true); + + List ALL = getTypes(null, false, true); + + static List getTypes(OsType osType, boolean remote, boolean custom) { + var all = new ArrayList(); + if (osType == null || osType.equals(OsType.WINDOWS)) { + all.addAll(WINDOWS_TERMINALS); + } + if (osType == null || osType.equals(OsType.LINUX)) { + all.addAll(LINUX_TERMINALS); + } + if (osType == null || osType.equals(OsType.MACOS)) { + all.addAll(MACOS_TERMINALS); + } + if (remote) { + all.removeIf(externalTerminalType -> externalTerminalType.remoteLaunchCommand() == null); + } + // Prefer recommended + all.sort(Comparator.comparingInt(o -> (o.isRecommended() ? -1 : 0))); + if (custom) { + all.add(CUSTOM); + } + return all; + } static ExternalTerminalType determineDefault(ExternalTerminalType existing) { // Check for incompatibility with fallback shell @@ -618,7 +648,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { return existing; } - return ALL.stream() + return APPLICABLE.stream() .filter(externalTerminalType -> !externalTerminalType.equals(CUSTOM)) .filter(terminalType -> terminalType.isAvailable()) .findFirst() @@ -641,6 +671,10 @@ public interface ExternalTerminalType extends PrefsChoiceValue { default void launch(LaunchConfiguration configuration) throws Exception {} + default FailableFunction remoteLaunchCommand() { + return null; + } + abstract class WindowsType extends ExternalApplicationType.WindowsType implements ExternalTerminalType { public WindowsType(String id, String executable) { @@ -715,6 +749,18 @@ public interface ExternalTerminalType extends PrefsChoiceValue { launch(configuration.getColoredTitle(), args); } + @Override + public FailableFunction remoteLaunchCommand() { + return launchConfiguration -> { + var args = toCommand(launchConfiguration); + args.add(0, executable); + if (explicityAsync) { + args = launchConfiguration.getScriptDialect().launchAsnyc(args); + } + return args.buildSimple(); + }; + } + protected abstract CommandBuilder toCommand(LaunchConfiguration configuration) throws Exception; } } diff --git a/app/src/main/java/io/xpipe/app/storage/StorageJacksonModule.java b/app/src/main/java/io/xpipe/app/util/AppJacksonModule.java similarity index 83% rename from app/src/main/java/io/xpipe/app/storage/StorageJacksonModule.java rename to app/src/main/java/io/xpipe/app/util/AppJacksonModule.java index 537b8de5..7c61b6c0 100644 --- a/app/src/main/java/io/xpipe/app/storage/StorageJacksonModule.java +++ b/app/src/main/java/io/xpipe/app/util/AppJacksonModule.java @@ -1,4 +1,4 @@ -package io.xpipe.app.storage; +package io.xpipe.app.util; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -6,8 +6,8 @@ import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; -import io.xpipe.app.util.PasswordLockSecretValue; -import io.xpipe.app.util.VaultKeySecretValue; +import io.xpipe.app.storage.*; +import io.xpipe.app.terminal.ExternalTerminalType; import io.xpipe.core.store.LocalStore; import io.xpipe.core.util.EncryptedSecretValue; import io.xpipe.core.util.JacksonMapper; @@ -16,7 +16,7 @@ import io.xpipe.core.util.SecretValue; import java.io.IOException; import java.util.UUID; -public class StorageJacksonModule extends SimpleModule { +public class AppJacksonModule extends SimpleModule { @Override public void setupModule(SetupContext context) { @@ -29,6 +29,8 @@ public class StorageJacksonModule extends SimpleModule { addDeserializer(ContextualFileReference.class, new LocalFileReferenceDeserializer()); addSerializer(DataStoreSecret.class, new DataStoreSecretSerializer()); addDeserializer(DataStoreSecret.class, new DataStoreSecretDeserializer()); + addSerializer(ExternalTerminalType.class, new ExternalTerminalTypeSerializer()); + addDeserializer(ExternalTerminalType.class, new ExternalTerminalTypeDeserializer()); context.addSerializers(_serializers); context.addDeserializers(_deserializers); @@ -51,6 +53,24 @@ public class StorageJacksonModule extends SimpleModule { } } + public static class ExternalTerminalTypeSerializer extends JsonSerializer { + + @Override + public void serialize(ExternalTerminalType value, JsonGenerator jgen, SerializerProvider provider) + throws IOException { + jgen.writeString(value.getId()); + } + } + + public static class ExternalTerminalTypeDeserializer extends JsonDeserializer { + + @Override + public ExternalTerminalType deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + var id = p.getValueAsString(); + return ExternalTerminalType.ALL.stream().filter(terminalType -> terminalType.getId().equals(id)).findFirst().orElse(null); + } + } + public static class DataStoreSecretSerializer extends JsonSerializer { @Override diff --git a/app/src/main/java/module-info.java b/app/src/main/java/module-info.java index 4320a853..8146b449 100644 --- a/app/src/main/java/module-info.java +++ b/app/src/main/java/module-info.java @@ -7,7 +7,7 @@ import io.xpipe.app.ext.*; import io.xpipe.app.issue.EventHandler; import io.xpipe.app.issue.EventHandlerImpl; import io.xpipe.app.storage.DataStateProviderImpl; -import io.xpipe.app.storage.StorageJacksonModule; +import io.xpipe.app.util.AppJacksonModule; import io.xpipe.app.util.LicenseProvider; import io.xpipe.app.util.ProxyManagerProviderImpl; import io.xpipe.app.util.TerminalLauncher; @@ -114,8 +114,7 @@ open module io.xpipe.app { uses LicenseProvider; uses io.xpipe.app.util.LicensedFeature; - provides Module with - StorageJacksonModule; + provides Module with AppJacksonModule; provides ModuleLayerLoader with MessageExchangeImpls.Loader, DataStoreProviders.Loader, diff --git a/core/src/main/java/io/xpipe/core/store/SingletonSessionStore.java b/core/src/main/java/io/xpipe/core/store/SingletonSessionStore.java new file mode 100644 index 00000000..681df6c9 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/SingletonSessionStore.java @@ -0,0 +1,51 @@ +package io.xpipe.core.store; + +public interface SingletonSessionStore extends InternalCacheDataStore { + + static abstract class Session { + + public abstract boolean isRunning(); + + public abstract void start() throws Exception; + + public abstract void stop() throws Exception; + } + + void onSessionUpdate(boolean active); + + T newSession() throws Exception; + + Class getSessionClass(); + + @SuppressWarnings("unchecked") + default T getSession() { + return (T) getCache("session", getSessionClass(), null); + } + + default void startIfNeeded() throws Exception { + var s = getSession(); + if (s != null) { + if (s.isRunning()) { + return; + } + + s.start(); + return; + } + + s = newSession(); + s.start(); + setCache("session", s); + onSessionUpdate(true); + } + + + default void stopIfNeeded() throws Exception { + var ex = getSession(); + setCache("session", null); + if (ex != null) { + ex.stop(); + onSessionUpdate(false); + } + } +} diff --git a/core/src/main/java/io/xpipe/core/store/SingletonToggleSessionStore.java b/core/src/main/java/io/xpipe/core/store/SingletonToggleSessionStore.java new file mode 100644 index 00000000..c72d3f90 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/SingletonToggleSessionStore.java @@ -0,0 +1,33 @@ +package io.xpipe.core.store; + +public interface SingletonToggleSessionStore extends SingletonSessionStore, StatefulDataStore, ValidatableStore { + + @Override + default Class getStateClass() { + return ToggleSessionState.class; + } + + @Override + public default void onSessionUpdate(boolean active) { + var c = getState(); + c.setRunning(active); + if (!active) { + c.setEnabled(false); + } else { + c.setEnabled(true); + } + setState(c); + } + + @Override + public default void validate() throws Exception { + if (getState().getEnabled() != null) { + if (getState().getEnabled()) { + startIfNeeded(); + } else { + stopIfNeeded(); + } + } + } + +} diff --git a/core/src/main/java/io/xpipe/core/store/ToggleSessionState.java b/core/src/main/java/io/xpipe/core/store/ToggleSessionState.java new file mode 100644 index 00000000..2a93b63b --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/ToggleSessionState.java @@ -0,0 +1,23 @@ +package io.xpipe.core.store; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +@Getter +@Setter +@SuperBuilder +@Jacksonized +public class ToggleSessionState extends DataStoreState { + + Boolean enabled; + Boolean running; + + @Override + public void merge(DataStoreState newer) { + var state = (ToggleSessionState) newer; + enabled = useNewer(enabled, state.enabled); + running = useNewer(running, state.running); + } + } \ No newline at end of file diff --git a/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopApplicationStore.java b/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopApplicationStore.java new file mode 100644 index 00000000..4ad57bcf --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopApplicationStore.java @@ -0,0 +1,36 @@ +package io.xpipe.ext.base.desktop; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.app.storage.ContextualFileReference; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.util.Validators; +import io.xpipe.core.process.CommandBuilder; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.util.JacksonizedValue; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +@Getter +@SuperBuilder +@Jacksonized +@JsonTypeName("desktopApplication") +public class DesktopApplicationStore extends JacksonizedValue implements DataStore { + + private final DataStoreEntryRef desktop; + private final ContextualFileReference path; + private final String arguments; + + @Override + public void checkComplete() throws Throwable { + Validators.nonNull(desktop); + Validators.isType(desktop, DesktopBaseStore.class); + Validators.nonNull(path); + } + + public String getFullCommand() { + var builder = CommandBuilder.of().addFile(path.toAbsoluteFilePath(null)).add(arguments != null ? " " + arguments : ""); + builder = desktop.getStore().getUsedDialect().launchAsnyc(builder); + return builder.buildSimple(); + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopApplicationStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopApplicationStoreProvider.java new file mode 100644 index 00000000..16b907f3 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopApplicationStoreProvider.java @@ -0,0 +1,124 @@ +package io.xpipe.ext.base.desktop; + +import io.xpipe.app.browser.session.BrowserSessionModel; +import io.xpipe.app.comp.store.StoreEntryWrapper; +import io.xpipe.app.comp.store.StoreViewState; +import io.xpipe.app.ext.ActionProvider; +import io.xpipe.app.ext.DataStoreProvider; +import io.xpipe.app.ext.GuiDialog; +import io.xpipe.app.fxcomps.impl.DataStoreChoiceComp; +import io.xpipe.app.storage.ContextualFileReference; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.util.DataStoreFormatter; +import io.xpipe.app.util.OptionsBuilder; +import io.xpipe.core.store.DataStore; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; + +import java.util.List; + +public class DesktopApplicationStoreProvider implements DataStoreProvider { + + @Override + public ActionProvider.Action browserAction(BrowserSessionModel sessionModel, DataStoreEntry store, BooleanProperty busy) { + DesktopApplicationStore s = store.getStore().asNeeded(); + return new ActionProvider.Action() { + @Override + public boolean requiresJavaFXPlatform() { + return false; + } + + @Override + public void execute() throws Exception { + s.getDesktop().getStore().runScript(store.getName(), s.getDesktop().getStore().getUsedDialect(), s.getFullCommand()); + } + }; + } + + @Override + public ActionProvider.Action launchAction(DataStoreEntry store) { + DesktopApplicationStore s = store.getStore().asNeeded(); + return new ActionProvider.Action() { + @Override + public boolean requiresJavaFXPlatform() { + return false; + } + + @Override + public void execute() throws Exception { + s.getDesktop().getStore().runScript(store.getName(), s.getDesktop().getStore().getUsedDialect(), s.getFullCommand()); + } + }; + } + + @Override + public CreationCategory getCreationCategory() { + return CreationCategory.DESKSTOP; + } + + @Override + public DataStoreEntry getDisplayParent(DataStoreEntry store) { + DesktopApplicationStore s = store.getStore().asNeeded(); + return s.getDesktop().get(); + } + + @Override + public GuiDialog guiDialog(DataStoreEntry entry, Property store) { + DesktopApplicationStore st = (DesktopApplicationStore) store.getValue(); + var host = new SimpleObjectProperty<>(st.getDesktop()); + var path = new SimpleStringProperty(st.getPath() != null ? st.getPath().toAbsoluteFilePath(null) : null); + var args = new SimpleStringProperty(st.getArguments() != null ? st.getArguments() : null); + return new OptionsBuilder() + .nameAndDescription("desktopEnvironmentBase") + .addComp( + new DataStoreChoiceComp<>( + DataStoreChoiceComp.Mode.HOST, + entry, + host, + DesktopBaseStore.class, + desktopStoreDataStoreEntryRef -> desktopStoreDataStoreEntryRef.getStore().supportsDesktopAccess(), + StoreViewState.get().getAllConnectionsCategory()), + host) + .nonNull() + .nameAndDescription("desktopApplicationPath") + .addString(path) + .nonNull() + .nameAndDescription("desktopApplicationArguments") + .addString(args) + .bind( + () -> { + return DesktopApplicationStore.builder().desktop(host.get()).path(ContextualFileReference.of(path.get())).arguments(args.get()).build(); + }, + store) + .buildDialog(); + } + + public String summaryString(StoreEntryWrapper wrapper) { + DesktopApplicationStore s = wrapper.getEntry().getStore().asNeeded(); + return DataStoreFormatter.toApostropheName(s.getDesktop().get()) + " config"; + } + + @Override + public String getDisplayIconFileName(DataStore store) { + return "base:desktopApplication_icon.svg"; + } + + @Override + public DataStore defaultStore() { + return DesktopApplicationStore.builder().build(); + } + + @Override + public List getPossibleNames() { + return List.of("desktopApplication"); + } + + @Override + public List> getStoreClasses() { + return List.of(DesktopApplicationStore.class); + } + + +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopBaseStore.java b/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopBaseStore.java new file mode 100644 index 00000000..9bbf02b2 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopBaseStore.java @@ -0,0 +1,19 @@ +package io.xpipe.ext.base.desktop; + +import io.xpipe.app.terminal.ExternalTerminalType; +import io.xpipe.core.process.OsType; +import io.xpipe.core.process.ShellDialect; +import io.xpipe.core.store.DataStore; + +public interface DesktopBaseStore extends DataStore { + + boolean supportsDesktopAccess(); + + void runScript(String name, ShellDialect dialect, String script) throws Exception; + + void runTerminal(String name, ExternalTerminalType terminalType, ShellDialect dialect, String script) throws Exception; + + ShellDialect getUsedDialect(); + + OsType getUsedOsType(); +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopEnvironmentStore.java b/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopEnvironmentStore.java new file mode 100644 index 00000000..08f6726c --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopEnvironmentStore.java @@ -0,0 +1,88 @@ +package io.xpipe.ext.base.desktop; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.app.storage.DataStoreEntryRef; +import io.xpipe.app.terminal.ExternalTerminalType; +import io.xpipe.app.util.Validators; +import io.xpipe.core.process.OsType; +import io.xpipe.core.process.ShellDialect; +import io.xpipe.core.store.DataStore; +import io.xpipe.core.util.JacksonizedValue; +import io.xpipe.ext.base.SelfReferentialStore; +import io.xpipe.ext.base.script.ScriptStore; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.util.List; +import java.util.stream.Stream; + +@Getter +@SuperBuilder +@Jacksonized +@JsonTypeName("desktopEnvironment") +public class DesktopEnvironmentStore extends JacksonizedValue implements DesktopBaseStore, DataStore, SelfReferentialStore { + + private final DataStoreEntryRef base; + private final ExternalTerminalType terminal; + private final ShellDialect dialect; + private final List> scripts; + private final String initScript; + + @Override + public void checkComplete() throws Throwable { + Validators.nonNull(base); + Validators.isType(base, DesktopBaseStore.class); + Validators.nonNull(terminal); + Validators.nonNull(dialect); + } + + public List> getEffectiveScripts() { + return scripts != null + ? scripts.stream().filter(scriptStore -> scriptStore != null).toList() + : List.of(); + } + + public String getMergedInitCommands(String command) { + var f = ScriptStore.flatten(scripts); + var filtered = f.stream().filter(simpleScriptStore -> simpleScriptStore.getMinimumDialect().isCompatibleTo(dialect) && simpleScriptStore.getExecutionType().runInTerminal()).toList(); + var initCommands = Stream.concat(filtered.stream().map(simpleScriptStore -> simpleScriptStore.getCommands()), command != null ? Stream.of(command) : Stream.of()).toList(); + var joined = String.join(dialect.getNewLine().getNewLineString(), initCommands); + return !joined.isBlank() ? joined : null; + } + + public void launch(String n, String commands) throws Exception { + var fullName = n + " [" + getSelfEntry().getName() + "]"; + base.getStore().runScript(fullName, dialect, getMergedInitCommands(commands)); + } + + public void launchSelf() throws Exception { + var fullName = getSelfEntry().getName(); + base.getStore().runTerminal(fullName, terminal, dialect, getMergedInitCommands(null)); + } + + @Override + public boolean supportsDesktopAccess() { + return base.getStore().supportsDesktopAccess(); + } + + @Override + public void runScript(String name, ShellDialect dialect, String script) throws Exception { + base.getStore().runScript(name, dialect, script); + } + + @Override + public void runTerminal(String name, ExternalTerminalType terminalType, ShellDialect dialect, String script) throws Exception { + base.getStore().runTerminal(name,terminalType,dialect,script); + } + + @Override + public ShellDialect getUsedDialect() { + return dialect; + } + + @Override + public OsType getUsedOsType() { + return base != null ? base.getStore().getUsedOsType() : null; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopEnvironmentStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopEnvironmentStoreProvider.java new file mode 100644 index 00000000..2f3d31bb --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/desktop/DesktopEnvironmentStoreProvider.java @@ -0,0 +1,159 @@ +package io.xpipe.ext.base.desktop; + +import io.xpipe.app.browser.session.BrowserSessionModel; +import io.xpipe.app.comp.base.IntegratedTextAreaComp; +import io.xpipe.app.comp.store.StoreEntryWrapper; +import io.xpipe.app.comp.store.StoreViewState; +import io.xpipe.app.core.AppExtensionManager; +import io.xpipe.app.ext.ActionProvider; +import io.xpipe.app.ext.DataStoreProvider; +import io.xpipe.app.ext.GuiDialog; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.impl.ChoiceComp; +import io.xpipe.app.fxcomps.impl.DataStoreChoiceComp; +import io.xpipe.app.fxcomps.impl.DataStoreListChoiceComp; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.terminal.ExternalTerminalType; +import io.xpipe.app.util.DataStoreFormatter; +import io.xpipe.app.util.OptionsBuilder; +import io.xpipe.core.store.DataStore; +import io.xpipe.ext.base.script.ScriptStore; +import javafx.beans.binding.Bindings; +import javafx.beans.property.*; +import javafx.collections.FXCollections; +import lombok.SneakyThrows; + +import java.util.ArrayList; +import java.util.List; + +public class DesktopEnvironmentStoreProvider implements DataStoreProvider { + + @Override + public ActionProvider.Action browserAction(BrowserSessionModel sessionModel, DataStoreEntry store, BooleanProperty busy) { + return launchAction(store); + } + + @Override + public ActionProvider.Action launchAction(DataStoreEntry store) { + DesktopEnvironmentStore s = store.getStore().asNeeded(); + return new ActionProvider.Action() { + @Override + public boolean requiresJavaFXPlatform() { + return false; + } + + @Override + public void execute() throws Exception { + s.launchSelf(); + } + }; + } + + @Override + public CreationCategory getCreationCategory() { + return CreationCategory.DESKSTOP; + } + + @Override + public DataStoreEntry getDisplayParent(DataStoreEntry store) { + DesktopEnvironmentStore s = store.getStore().asNeeded(); + return s.getBase().get(); + } + + @Override + @SneakyThrows + public GuiDialog guiDialog(DataStoreEntry entry, Property store) { + DesktopEnvironmentStore st = (DesktopEnvironmentStore) store.getValue(); + var host = new SimpleObjectProperty<>(st.getBase()); + var terminal = new SimpleObjectProperty<>(st.getTerminal()); + var dialect = new SimpleObjectProperty<>(st.getDialect()); + var scripts = new SimpleListProperty<>(FXCollections.observableArrayList(new ArrayList<>(st.getEffectiveScripts()))); + var initScript = new SimpleStringProperty(st.getInitScript()); + + + Comp dialectChoice = (Comp) Class.forName( + AppExtensionManager.getInstance() + .getExtendedLayer() + .findModule("io.xpipe.ext.proc") + .orElseThrow(), + "io.xpipe.ext.proc.ShellDialectChoiceComp") + .getDeclaredConstructor(Property.class) + .newInstance(dialect); + return new OptionsBuilder() + .nameAndDescription("desktopHost") + .addComp( + new DataStoreChoiceComp<>( + DataStoreChoiceComp.Mode.HOST, + entry, + host, + DesktopBaseStore.class, + desktopStoreDataStoreEntryRef -> desktopStoreDataStoreEntryRef.getStore().supportsDesktopAccess(), + StoreViewState.get().getAllConnectionsCategory()), + host) + .nonNull() + .nameAndDescription("desktopTerminal") + .addComp(ChoiceComp.ofTranslatable(terminal, ExternalTerminalType.getTypes(st.getUsedOsType(), true, false),true), terminal) + .nonNull() + .nameAndDescription("desktopShellDialect") + .addComp(dialectChoice, dialect) + .nonNull() + .nameAndDescription("desktopSnippets") + .addComp( + new DataStoreListChoiceComp<>( + scripts, + ScriptStore.class, + scriptStore -> !scripts.contains(scriptStore), + StoreViewState.get().getAllScriptsCategory()), + scripts) + .nameAndDescription("desktopInitScript") + .longDescription("proc:environmentScript") + .addComp( + new IntegratedTextAreaComp( + initScript, + false, + "commands", + Bindings.createStringBinding( + () -> { + return dialect.getValue() != null + ? dialect + .getValue() + .getScriptFileEnding() + : "sh"; + }, + dialect)), + initScript) + .bind( + () -> { + return DesktopEnvironmentStore.builder().base(host.get()).terminal(terminal.get()).dialect(dialect.get()).scripts(scripts.get()).initScript(initScript.get()).build(); + }, + store) + .buildDialog(); + } + + public String summaryString(StoreEntryWrapper wrapper) { + DesktopEnvironmentStore s = wrapper.getEntry().getStore().asNeeded(); + return DataStoreFormatter.toApostropheName(s.getBase().get()) + " environment"; + } + + @Override + public String getDisplayIconFileName(DataStore store) { + return "base:desktopEnvironment_icon.svg"; + } + + @Override + public DataStore defaultStore() { + return DesktopEnvironmentStore.builder().build(); + } + + @Override + public List getPossibleNames() { + return List.of("desktopEnvironment"); + } + + @Override + public List> getStoreClasses() { + return List.of(DesktopEnvironmentStore.class); + } + + +} diff --git a/ext/base/src/main/java/module-info.java b/ext/base/src/main/java/module-info.java index 0b6b8209..3e9aff81 100644 --- a/ext/base/src/main/java/module-info.java +++ b/ext/base/src/main/java/module-info.java @@ -3,8 +3,10 @@ import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.ext.DataStoreProvider; import io.xpipe.ext.base.action.*; import io.xpipe.ext.base.browser.*; +import io.xpipe.ext.base.desktop.DesktopEnvironmentStoreProvider; import io.xpipe.ext.base.script.ScriptGroupStoreProvider; import io.xpipe.ext.base.script.SimpleScriptStoreProvider; +import io.xpipe.ext.base.desktop.DesktopApplicationStoreProvider; import io.xpipe.ext.base.store.StorePauseAction; import io.xpipe.ext.base.store.StoreStartAction; import io.xpipe.ext.base.store.StoreStopAction; @@ -14,6 +16,7 @@ open module io.xpipe.ext.base { exports io.xpipe.ext.base.action; exports io.xpipe.ext.base.script; exports io.xpipe.ext.base.store; + exports io.xpipe.ext.base.desktop; requires java.desktop; requires io.xpipe.core; @@ -64,6 +67,6 @@ open module io.xpipe.ext.base { DeleteStoreChildrenAction, BrowseStoreAction; provides DataStoreProvider with - SimpleScriptStoreProvider, + SimpleScriptStoreProvider, DesktopEnvironmentStoreProvider, DesktopApplicationStoreProvider, ScriptGroupStoreProvider; } diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-dark.svg b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-dark.svg new file mode 100644 index 00000000..9186ef3a --- /dev/null +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon-dark.svg @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon.svg b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon.svg new file mode 100644 index 00000000..f0f5b4ef --- /dev/null +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopApplication_icon.svg @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopCommand_icon-dark.svg b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopCommand_icon-dark.svg new file mode 100644 index 00000000..d8107c4e --- /dev/null +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopCommand_icon-dark.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopCommand_icon.svg b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopCommand_icon.svg new file mode 100644 index 00000000..024408ba --- /dev/null +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopCommand_icon.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopEnvironment_icon-dark.svg b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopEnvironment_icon-dark.svg new file mode 100644 index 00000000..f9f46709 --- /dev/null +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopEnvironment_icon-dark.svg @@ -0,0 +1,68 @@ + + + + + + + shell environment v2 + + + + + + + shell environment v2 + + + + diff --git a/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopEnvironment_icon.svg b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopEnvironment_icon.svg new file mode 100644 index 00000000..22440eeb --- /dev/null +++ b/ext/base/src/main/resources/io/xpipe/ext/base/resources/img/desktopEnvironment_icon.svg @@ -0,0 +1,68 @@ + + + + + + + shell environment v2 + + + + + + + shell environment v2 + + + + diff --git a/lang/app/strings/translations_en.properties b/lang/app/strings/translations_en.properties index 2e29a88c..76b4ae54 100644 --- a/lang/app/strings/translations_en.properties +++ b/lang/app/strings/translations_en.properties @@ -430,6 +430,7 @@ goodMorning=Good morning goodAfternoon=Good afternoon goodEvening=Good evening addVisual=Visual ... +addDesktop=Desktop ... ssh=SSH sshConfiguration=SSH Configuration size=Size diff --git a/lang/base/strings/translations_en.properties b/lang/base/strings/translations_en.properties index 2ac1eff2..31bdfeee 100644 --- a/lang/base/strings/translations_en.properties +++ b/lang/base/strings/translations_en.properties @@ -106,6 +106,25 @@ openInTerminal=Open in terminal file=File directory=Directory symbolicLink=Symbolic link - +desktopEnvironment.displayName=Desktop environment +desktopEnvironment.displayDescription=Create a reusable desktop environment configuration +desktopHost=Desktop host +desktopHostDescription=The desktop connection to use as a base +desktopShellDialect=Shell dialect +desktopShellDialectDescription=The shell dialect to use to run scripts and applications +desktopSnippets=Script snippets +desktopSnippetsDescription=List of reusable script snippets to run first +desktopInitScript=Init script +desktopInitScriptDescription=Init commands specific to this environment +desktopTerminal=Terminal application +desktopTerminalDescription=The terminal to use on the desktop to start scripts in +desktopApplication.displayName=Desktop application +desktopApplication.displayDescription=Run an application in a desktop environment +desktopEnvironmentBase=Desktop environment +desktopEnvironmentBaseDescription=The desktop environment to run this application on +desktopApplicationPath=Application path +desktopApplicationPathDescription=The path of the executable to run +desktopApplicationArguments=Arguments +desktopApplicationArgumentsDescription=The optional arguments to pass to the application