This commit is contained in:
crschnick
2026-01-20 05:52:32 +00:00
parent 8a06fe4f72
commit bd7ced4367
29 changed files with 465 additions and 179 deletions

View File

@@ -43,6 +43,20 @@ public class IntegratedTextAreaComp extends Comp<IntegratedTextAreaComp.Structur
public static IntegratedTextAreaComp script(
ObservableValue<DataStoreEntryRef<ShellStore>> host, Property<ShellScript> value) {
var type = Bindings.createStringBinding(
() -> {
return host.getValue() != null
&& host.getValue().getStore() instanceof StatefulDataStore<?> sd
&& sd.getState() instanceof SystemState ss
&& ss.getShellDialect() != null
? ss.getShellDialect().getScriptFileEnding()
: "sh";
},
host);
return script(value, type);
}
public static IntegratedTextAreaComp script(Property<ShellScript> value, ObservableValue<String> fileType) {
var string = new SimpleStringProperty();
value.subscribe(shellScript -> {
string.set(shellScript != null ? shellScript.getValue() : null);
@@ -54,16 +68,7 @@ public class IntegratedTextAreaComp extends Comp<IntegratedTextAreaComp.Structur
string,
false,
"script",
Bindings.createStringBinding(
() -> {
return host.getValue() != null
&& host.getValue().getStore() instanceof StatefulDataStore<?> sd
&& sd.getState() instanceof SystemState ss
&& ss.getShellDialect() != null
? ss.getShellDialect().getScriptFileEnding()
: "sh";
},
host));
fileType);
return i;
}

View File

@@ -40,6 +40,10 @@ public class AppImages {
var exts = AppExtensionManager.getInstance().getContentModules();
for (Module ext : exts) {
AppResources.with(ext.getName(), "img/", basePath -> {
if (!Files.exists(basePath)) {
return;
}
var skipLarge = AppDisplayScale.hasDefaultDisplayScale();
Files.walkFileTree(basePath, new SimpleFileVisitor<>() {
@Override

View File

@@ -23,6 +23,15 @@ public class ShellScript {
return new ShellScript(lines.stream().collect(Collectors.joining("\n")));
}
public String withoutShebang() {
var shebang = value.startsWith("#!");
if (shebang) {
return value.lines().skip(1).collect(Collectors.joining("\n"));
} else {
return value;
}
}
@Override
public String toString() {
return value;

View File

@@ -2,7 +2,9 @@ package io.xpipe.ext.base.script;
import io.xpipe.app.core.AppNames;
import io.xpipe.app.core.AppResources;
import io.xpipe.app.process.ShellDialect;
import io.xpipe.app.process.ShellDialects;
import io.xpipe.app.process.ShellScript;
import io.xpipe.app.storage.DataStoreEntryRef;
import lombok.Getter;
@@ -18,34 +20,29 @@ import java.util.function.Supplier;
public enum PredefinedScriptStore {
APT_UPDATE("Apt upgrade", () -> SimpleScriptStore.builder()
.group(PredefinedScriptGroup.MANAGEMENT.getEntry())
.minimumDialect(ShellDialects.SH)
.commands(file(("apt_upgrade.sh")))
.textSource(ScriptTextSource.InPlace.builder().dialect(ShellDialects.SH).text(file("apt_upgrade.sh")).build())
.shellScript(true)
.runnableScript(true)
.build()),
REMOVE_CR("CRLF to LF", () -> SimpleScriptStore.builder()
.group(PredefinedScriptGroup.FILES.getEntry())
.minimumDialect(ShellDialects.SH)
.commands(file(("crlf_to_lf.sh")))
.textSource(ScriptTextSource.InPlace.builder().dialect(ShellDialects.SH).text(file("crlf_to_lf.sh")).build())
.fileScript(true)
.shellScript(true)
.build()),
DIFF("Diff", () -> SimpleScriptStore.builder()
.group(PredefinedScriptGroup.FILES.getEntry())
.minimumDialect(ShellDialects.SH)
.commands(file(("diff.sh")))
.textSource(ScriptTextSource.InPlace.builder().dialect(ShellDialects.SH).text(file("diff.sh")).build())
.fileScript(true)
.build()),
GIT_CONFIG("Git Config", () -> SimpleScriptStore.builder()
.group(PredefinedScriptGroup.MANAGEMENT.getEntry())
.minimumDialect(null)
.commands(file(("git_config.sh")))
.textSource(ScriptTextSource.InPlace.builder().text(file("git_config.sh")).build())
.runnableScript(true)
.build()),
SYSTEM_HEALTH_STATUS("System health status", () -> SimpleScriptStore.builder()
.group(PredefinedScriptGroup.MANAGEMENT.getEntry())
.minimumDialect(ShellDialects.SH)
.commands(file(("system_health.sh")))
.textSource(ScriptTextSource.InPlace.builder().text(file("system_health.sh")).build())
.initScript(true)
.build());
@@ -62,11 +59,11 @@ public enum PredefinedScriptStore {
this.uuid = UUID.nameUUIDFromBytes(name.getBytes(StandardCharsets.UTF_8));
}
public static String file(String name) {
public static ShellScript file(String name) {
AtomicReference<String> string = new AtomicReference<>();
AppResources.with(AppNames.extModuleName("base"), "scripts/" + name, var1 -> {
string.set(Files.readString(var1));
});
return string.get();
return ShellScript.of(string.get());
}
}

View File

@@ -7,12 +7,9 @@ import io.xpipe.app.comp.base.ContextualFileReferenceChoiceComp;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.ext.ShellDialectChoiceComp;
import io.xpipe.app.ext.ValidationException;
import io.xpipe.app.icon.SystemIconSource;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.process.ShellDialects;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.Validators;
import io.xpipe.core.FilePath;
@@ -32,16 +29,16 @@ import java.util.List;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = ScriptSource.Directory.class),
@JsonSubTypes.Type(value = ScriptSource.GitRepository.class)
@JsonSubTypes.Type(value = ScriptCollectionSource.Directory.class),
@JsonSubTypes.Type(value = ScriptCollectionSource.GitRepository.class)
})
public interface ScriptSource {
public interface ScriptCollectionSource {
@JsonTypeName("directory")
@Value
@Jacksonized
@Builder
class Directory implements ScriptSource {
class Directory implements ScriptCollectionSource {
Path path;
@@ -92,7 +89,7 @@ public interface ScriptSource {
@Value
@Jacksonized
@Builder
class GitRepository implements ScriptSource {
class GitRepository implements ScriptCollectionSource {
String url;
@@ -157,16 +154,9 @@ public interface ScriptSource {
String toName();
default List<ScriptSourceEntry> listScripts() throws Exception {
var availableDialects = List.of(
ShellDialects.SH,
ShellDialects.BASH,
ShellDialects.ZSH,
ShellDialects.FISH,
ShellDialects.CMD,
ShellDialects.POWERSHELL,
ShellDialects.POWERSHELL_CORE);
var l = new ArrayList<ScriptSourceEntry>();
default List<ScriptCollectionSourceEntry> listScripts() throws Exception {
var availableDialects = ScriptDialects.getSupported();
var l = new ArrayList<ScriptCollectionSourceEntry>();
Files.walkFileTree(getLocalPath(), new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
@@ -178,9 +168,9 @@ public interface ScriptSource {
return FileVisitResult.CONTINUE;
}
var entry = ScriptSourceEntry.builder()
var entry = ScriptCollectionSourceEntry.builder()
.name(name)
.source(ScriptSource.this)
.source(ScriptCollectionSource.this)
.dialect(dialect.get())
.localFile(file)
.build();

View File

@@ -1,24 +1,20 @@
package io.xpipe.ext.base.script;
import io.xpipe.app.action.AbstractAction;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.FileSystemStore;
import io.xpipe.app.hub.action.HubLeafProvider;
import io.xpipe.app.hub.action.StoreAction;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.DesktopHelper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
public class ScriptSourceBrowseActionProvider implements HubLeafProvider<ScriptSourceStore> {
public class ScriptCollectionSourceBrowseActionProvider implements HubLeafProvider<ScriptCollectionSourceStore> {
@Override
public AbstractAction createAction(DataStoreEntryRef<ScriptSourceStore> ref) {
public AbstractAction createAction(DataStoreEntryRef<ScriptCollectionSourceStore> ref) {
return Action.builder().ref(ref).build();
}
@@ -28,28 +24,28 @@ public class ScriptSourceBrowseActionProvider implements HubLeafProvider<ScriptS
}
@Override
public ObservableValue<String> getName(DataStoreEntryRef<ScriptSourceStore> store) {
public ObservableValue<String> getName(DataStoreEntryRef<ScriptCollectionSourceStore> store) {
return AppI18n.observable("browse");
}
@Override
public LabelGraphic getIcon(DataStoreEntryRef<ScriptSourceStore> store) {
public LabelGraphic getIcon(DataStoreEntryRef<ScriptCollectionSourceStore> store) {
return new LabelGraphic.IconGraphic("mdi2f-folder-search-outline");
}
@Override
public Class<ScriptSourceStore> getApplicableClass() {
return ScriptSourceStore.class;
public Class<ScriptCollectionSourceStore> getApplicableClass() {
return ScriptCollectionSourceStore.class;
}
@Override
public String getId() {
return "browseScriptSource";
return "browseScriptCollectionSource";
}
@Jacksonized
@SuperBuilder
public static class Action extends StoreAction<ScriptSourceStore> {
public static class Action extends StoreAction<ScriptCollectionSourceStore> {
@Override
public void executeImpl() {

View File

@@ -10,10 +10,10 @@ import java.nio.file.Path;
@Value
@Builder
@Jacksonized
public class ScriptSourceEntry {
public class ScriptCollectionSourceEntry {
String name;
ShellDialect dialect;
ScriptSource source;
ScriptCollectionSource source;
Path localFile;
}

View File

@@ -1,20 +1,16 @@
package io.xpipe.ext.base.script;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.FixedHierarchyStore;
import io.xpipe.app.hub.action.BatchHubProvider;
import io.xpipe.app.hub.action.HubLeafProvider;
import io.xpipe.app.hub.action.StoreAction;
import io.xpipe.app.hub.action.StoreActionCategory;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.ext.base.service.FixedServiceGroupStore;
import javafx.beans.value.ObservableValue;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
public class ScriptSourceRefreshHubProvider implements HubLeafProvider<ScriptSourceStore> {
public class ScriptCollectionSourceRefreshHubProvider implements HubLeafProvider<ScriptCollectionSourceStore> {
@Override
public StoreActionCategory getCategory() {
@@ -27,28 +23,28 @@ public class ScriptSourceRefreshHubProvider implements HubLeafProvider<ScriptSou
}
@Override
public ObservableValue<String> getName(DataStoreEntryRef<ScriptSourceStore> store) {
public ObservableValue<String> getName(DataStoreEntryRef<ScriptCollectionSourceStore> store) {
return AppI18n.observable("refreshSource");
}
@Override
public LabelGraphic getIcon(DataStoreEntryRef<ScriptSourceStore> store) {
public LabelGraphic getIcon(DataStoreEntryRef<ScriptCollectionSourceStore> store) {
return new LabelGraphic.IconGraphic("mdi2r-refresh");
}
@Override
public Class<?> getApplicableClass() {
return ScriptSourceStore.class;
return ScriptCollectionSourceStore.class;
}
@Override
public String getId() {
return "refreshSource";
return "refreshScriptCollection";
}
@Jacksonized
@SuperBuilder
public static class Action extends StoreAction<ScriptSourceStore> {
public static class Action extends StoreAction<ScriptCollectionSourceStore> {
@Override
public void executeImpl() throws Exception {

View File

@@ -1,17 +1,8 @@
package io.xpipe.ext.base.script;
import com.fasterxml.jackson.annotation.JsonTypeId;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.app.ext.*;
import io.xpipe.app.process.ShellStoreState;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.HostHelper;
import io.xpipe.app.util.LicenseProvider;
import io.xpipe.app.util.Validators;
import io.xpipe.ext.base.host.HostAddressGatewayStore;
import io.xpipe.ext.base.service.ServiceAddressRotation;
import io.xpipe.ext.base.service.ServiceProtocolType;
import lombok.*;
import lombok.experimental.FieldDefaults;
import lombok.experimental.SuperBuilder;
@@ -22,10 +13,10 @@ import java.util.List;
@SuperBuilder(toBuilder = true)
@Value
@Jacksonized
@JsonTypeName("scriptSource")
public class ScriptSourceStore implements DataStore, StatefulDataStore<ScriptSourceStore.State> {
@JsonTypeName("scriptCollectionSource")
public class ScriptCollectionSourceStore implements DataStore, StatefulDataStore<ScriptCollectionSourceStore.State> {
ScriptSource source;
ScriptCollectionSource source;
@Override
public void checkComplete() throws Throwable {
@@ -46,7 +37,7 @@ public class ScriptSourceStore implements DataStore, StatefulDataStore<ScriptSou
@Jacksonized
public static class State extends DataStoreState {
List<ScriptSourceEntry> entries;
List<ScriptCollectionSourceEntry> entries;
@Override
public DataStoreState mergeCopy(DataStoreState newer) {

View File

@@ -1,40 +1,24 @@
package io.xpipe.ext.base.script;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.IntegratedTextAreaComp;
import io.xpipe.app.comp.base.ListSelectorComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.*;
import io.xpipe.app.hub.comp.*;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.platform.OptionsChoiceBuilder;
import io.xpipe.app.platform.Validator;
import io.xpipe.app.process.OsFileSystem;
import io.xpipe.app.process.ShellDialect;
import io.xpipe.app.process.ShellDialects;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.DataStoreFormatter;
import io.xpipe.app.util.DocumentationLink;
import io.xpipe.core.OsType;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import lombok.SneakyThrows;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Stream;
public class ScriptSourceStoreProvider implements DataStoreProvider {
public class ScriptCollectionSourceStoreProvider implements DataStoreProvider {
@Override
public UUID getTargetCategory(DataStore store, UUID target) {
@@ -69,18 +53,18 @@ public class ScriptSourceStoreProvider implements DataStoreProvider {
@SneakyThrows
@Override
public GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {
ScriptSourceStore st = store.getValue().asNeeded();
ScriptCollectionSourceStore st = store.getValue().asNeeded();
var source = new SimpleObjectProperty<>(st.getSource());
var sourceChoice = OptionsChoiceBuilder.builder().property(source).available(ScriptSource.getClasses()).build();
var sourceChoice = OptionsChoiceBuilder.builder().property(source).available(ScriptCollectionSource.getClasses()).build();
return new OptionsBuilder()
.nameAndDescription("scriptSourceType")
.nameAndDescription("scriptCollectionSourceType")
.sub(sourceChoice.build(), source)
.bind(
() -> {
return ScriptSourceStore.builder().source(source.get()).build();
return ScriptCollectionSourceStore.builder().source(source.get()).build();
},
store)
.buildDialog();
@@ -88,13 +72,13 @@ public class ScriptSourceStoreProvider implements DataStoreProvider {
@Override
public String summaryString(StoreEntryWrapper wrapper) {
ScriptSourceStore st = wrapper.getEntry().getStore().asNeeded();
ScriptCollectionSourceStore st = wrapper.getEntry().getStore().asNeeded();
return st.getSource().toName();
}
@Override
public ObservableValue<String> informationString(StoreSection section) {
ScriptSourceStore st = section.getWrapper().getEntry().getStore().asNeeded();
ScriptCollectionSourceStore st = section.getWrapper().getEntry().getStore().asNeeded();
return Bindings.createStringBinding(() -> {
var s = st.getState();
var summary = st.getSource().toSummary();
@@ -104,16 +88,16 @@ public class ScriptSourceStoreProvider implements DataStoreProvider {
@Override
public DataStore defaultStore(DataStoreCategory category) {
return ScriptSourceStore.builder().build();
return ScriptCollectionSourceStore.builder().build();
}
@Override
public String getId() {
return "scriptSource";
return "scriptCollectionSource";
}
@Override
public List<Class<?>> getStoreClasses() {
return List.of(ScriptSourceStore.class);
return List.of(ScriptCollectionSourceStore.class);
}
}

View File

@@ -0,0 +1,21 @@
package io.xpipe.ext.base.script;
import io.xpipe.app.process.ShellDialect;
import io.xpipe.app.process.ShellDialects;
import java.util.List;
public class ScriptDialects {
public static List<ShellDialect> getSupported() {
var availableDialects = List.of(
ShellDialects.SH,
ShellDialects.BASH,
ShellDialects.ZSH,
ShellDialects.FISH,
ShellDialects.CMD,
ShellDialects.POWERSHELL,
ShellDialects.POWERSHELL_CORE);
return availableDialects;
}
}

View File

@@ -71,7 +71,7 @@ public class ScriptStoreSetup {
pc.withInitSnippet(
new ShellTerminalInitCommand() {
String dir;
FilePath dir;
@Override
public Optional<String> terminalContent(ShellControl shellControl) throws Exception {
@@ -84,7 +84,7 @@ public class ScriptStoreSetup {
}
return Optional.ofNullable(
shellControl.getShellDialect().addToPathVariableCommand(List.of(dir), true));
shellControl.getShellDialect().addToPathVariableCommand(List.of(dir.toString()), true));
}
@Override
@@ -102,14 +102,14 @@ public class ScriptStoreSetup {
}
}
private static String initScriptsDirectory(ShellControl proc, List<DataStoreEntryRef<SimpleScriptStore>> refs)
private static FilePath initScriptsDirectory(ShellControl sc, List<DataStoreEntryRef<SimpleScriptStore>> refs)
throws Exception {
if (refs.isEmpty()) {
return null;
}
var applicable = refs.stream()
.filter(simpleScriptStore -> simpleScriptStore.getStore().isCompatible(proc.getShellDialect()))
.filter(simpleScriptStore -> simpleScriptStore.getStore().isCompatible(sc.getShellDialect()))
.toList();
if (applicable.isEmpty()) {
return null;
@@ -119,13 +119,11 @@ public class ScriptStoreSetup {
.mapToInt(value ->
value.get().getName().hashCode() + value.getStore().hashCode())
.sum();
var targetDir = ShellTemp.createUserSpecificTempDataDirectory(proc, "scripts")
.join(proc.getShellDialect().getId())
.toString();
var hashFile = FilePath.of(targetDir, "hash");
var d = proc.getShellDialect();
if (d.createFileExistsCommand(proc, hashFile.toString()).executeAndCheck()) {
var read = d.getFileReadCommand(proc, hashFile.toString()).readStdoutOrThrow();
var targetDir = ShellTemp.createUserSpecificTempDataDirectory(sc, "scripts")
.join(sc.getShellDialect().getId());
var hashFile = targetDir.join("hash");
if (sc.view().fileExists(hashFile)) {
var read = sc.view().readTextFile(hashFile);
try {
var readHash = Integer.parseInt(read);
if (hash == readHash) {
@@ -136,21 +134,23 @@ public class ScriptStoreSetup {
}
}
if (d.directoryExists(proc, targetDir).executeAndCheck()) {
d.deleteFileOrDirectory(proc, targetDir).execute();
if (sc.view().directoryExists(targetDir)) {
sc.view().deleteDirectory(targetDir);
}
proc.executeSimpleCommand(d.getMkdirsCommand(targetDir));
sc.view().mkdir(targetDir);
var d = sc.getShellDialect();
for (DataStoreEntryRef<SimpleScriptStore> scriptStore : refs) {
var content = d.prepareScriptContent(proc, scriptStore.getStore().getCommands());
var fileName = OsFileSystem.of(proc.getOsType())
var src = scriptStore.getStore().getTextSource();
var content = src.getText();
var fileName = OsFileSystem.of(sc.getOsType())
.makeFileSystemCompatible(
scriptStore.get().getName().toLowerCase(Locale.ROOT).replaceAll(" ", "_"));
var scriptFile = FilePath.of(targetDir, fileName + "." + d.getScriptFileEnding());
proc.view().writeScriptFile(scriptFile, content);
var scriptFile = targetDir.join(fileName + "." + d.getScriptFileEnding());
sc.view().writeScriptFile(scriptFile, content.getValue());
}
proc.view().writeTextFile(hashFile, String.valueOf(hash));
sc.view().writeTextFile(hashFile, String.valueOf(hash));
return targetDir;
}

View File

@@ -0,0 +1,241 @@
package io.xpipe.ext.base.script;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.ContextualFileReferenceChoiceComp;
import io.xpipe.app.comp.base.IntegratedTextAreaComp;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.ext.ShellDialectChoiceComp;
import io.xpipe.app.ext.ValidationException;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.process.ShellDialect;
import io.xpipe.app.process.ShellScript;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.DocumentationLink;
import io.xpipe.app.util.HttpHelper;
import io.xpipe.app.util.Validators;
import io.xpipe.core.FilePath;
import io.xpipe.core.UuidHelper;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import lombok.Builder;
import lombok.SneakyThrows;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = ScriptTextSource.InPlace.class),
@JsonSubTypes.Type(value = ScriptTextSource.Url.class),
@JsonSubTypes.Type(value = ScriptTextSource.SourceReference.class)
})
public interface ScriptTextSource {
@JsonTypeName("inPlace")
@Value
@Jacksonized
@Builder
class InPlace implements ScriptTextSource {
ShellDialect dialect;
ShellScript text;
@SuppressWarnings("unused")
public static String getOptionsNameKey() {
return "scriptSourceTypeInPlace";
}
@SuppressWarnings("unused")
static OptionsBuilder createOptions(Property<InPlace> property) {
var dialect = new SimpleObjectProperty<>(property.getValue().getDialect());
var text = new SimpleObjectProperty<>(property.getValue().getText());
var availableDialects = ScriptDialects.getSupported();
var choice = new ShellDialectChoiceComp(availableDialects, dialect, ShellDialectChoiceComp.NullHandling.NULL_IS_ALL);
return new OptionsBuilder()
.name("minimumShellDialect")
.description("minimumShellDialectDescription")
.documentationLink(DocumentationLink.SCRIPTING_COMPATIBILITY)
.addComp(choice, dialect)
.name("scriptContents")
.description("scriptContentsDescription")
.documentationLink(DocumentationLink.SCRIPTING_EDITING)
.addComp(IntegratedTextAreaComp.script(text, Bindings.createStringBinding(() -> {
return dialect.getValue() != null
? dialect.getValue().getScriptFileEnding()
: "sh";
}, dialect)))
.bind(() -> InPlace.builder().dialect(dialect.get()).text(text.get()).build(),
property);
}
@Override
public void checkComplete() throws ValidationException {
Validators.nonNull(text);
}
@Override
public String toSummary() {
return dialect.getDisplayName();
}
}
@JsonTypeName("url")
@Value
@Jacksonized
@Builder
class Url implements ScriptTextSource {
ShellDialect dialect;
String url;
ShellScript lastText;
@SuppressWarnings("unused")
public static String getOptionsNameKey() {
return "scriptSourceTypeUrl";
}
@SuppressWarnings("unused")
static OptionsBuilder createOptions(Property<Url> property) {
var dialect = new SimpleObjectProperty<>(property.getValue().getDialect());
var url = new SimpleStringProperty(property.getValue().getUrl());
var availableDialects = ScriptDialects.getSupported();
var choice = new ShellDialectChoiceComp(availableDialects, dialect, ShellDialectChoiceComp.NullHandling.NULL_IS_ALL);
return new OptionsBuilder()
.name("minimumShellDialect")
.description("minimumShellDialectDescription")
.documentationLink(DocumentationLink.SCRIPTING_COMPATIBILITY)
.addComp(choice, dialect)
.nameAndDescription("scriptTextSourceUrl").addString(url).nonNull().bind(
() -> Url.builder().dialect(dialect.get()).url(url.get()).build(), property);
}
private void prepare() throws Exception {
var path = getLocalPath();
if (Files.exists(path)) {
return;
}
var req = HttpRequest.newBuilder().GET().uri(URI.create(url)).build();
var r = HttpHelper.client().send(req, HttpResponse.BodyHandlers.ofString());
if (r.statusCode() >= 400) {
throw ErrorEventFactory.expected(new IOException(r.body()));
}
Files.writeString(path, r.body());
}
private Path getLocalPath() {
return AppCache.getBasePath().resolve("scripts").resolve(getName());
}
private String getName() {
var name = FilePath.of(url).getFileName();
if (!name.isEmpty()) {
return name;
}
return UuidHelper.generateFromObject(url).toString();
}
@Override
public void checkComplete() throws ValidationException {
Validators.nonNull(url);
Validators.nonNull(lastText);
}
@Override
public String toSummary() {
return url;
}
@Override
@SneakyThrows
public ShellScript getText() {
return lastText;
}
}
@JsonTypeName("source")
@Value
@Jacksonized
@Builder
class SourceReference implements ScriptTextSource {
ScriptCollectionSourceEntry entry;
@SuppressWarnings("unused")
public static String getOptionsNameKey() {
return "scriptSourceTypeSource";
}
@SuppressWarnings("unused")
static OptionsBuilder createOptions(Property<SourceReference> property) {
var entry = new SimpleObjectProperty<>(property.getValue().getEntry());
return new OptionsBuilder().bind(
() -> SourceReference.builder().entry(entry.get()).build(), property);
}
@Override
public void checkComplete() throws ValidationException {
Validators.nonNull(entry);
entry.getSource().checkComplete();
}
@Override
public String toSummary() {
return entry.getName();
}
@Override
public ShellDialect getDialect() {
return entry.getDialect();
}
@Override
@SneakyThrows
public ShellScript getText() {
var r = Files.readString(entry.getLocalFile());
return ShellScript.of(r);
}
}
void checkComplete() throws ValidationException;
String toSummary();
ShellDialect getDialect();
ShellScript getText();
static List<Class<?>> getClasses() {
var l = new ArrayList<Class<?>>();
l.add(InPlace.class);
l.add(Url.class);
l.add(SourceReference.class);
return l;
}
}

View File

@@ -0,0 +1,69 @@
package io.xpipe.ext.base.script;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.fasterxml.jackson.databind.node.TreeTraversingParser;
import io.xpipe.app.storage.DataStorage;
import java.io.IOException;
public class SimpleScriptMigrationDeserializer extends DelegatingDeserializer {
public SimpleScriptMigrationDeserializer(JsonDeserializer<?> d) {
super(d);
}
@Override
protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) {
return new SimpleScriptMigrationDeserializer(newDelegatee);
}
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return super.deserialize(restructure(p), ctxt);
}
@Override
public Object deserialize(JsonParser p, DeserializationContext ctxt, Object intoValue) throws IOException {
return super.deserialize(restructure(p), ctxt, intoValue);
}
public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer typeDeserializer)
throws IOException {
return super.deserializeWithType(restructure(jp), ctxt, typeDeserializer);
}
public JsonParser restructure(JsonParser p) throws IOException {
var node = p.readValueAsTree();
if (node == null || !node.isObject()) {
return p;
}
migrate((ObjectNode) node);
var newJsonParser = new TreeTraversingParser(((ObjectNode) node), p.getCodec());
newJsonParser.nextToken();
return newJsonParser;
}
private void migrate(ObjectNode n) {
var commandsNode = n.remove("commands");
var dialectNode = n.remove("minimumDialect");
var obj = JsonNodeFactory.instance.objectNode();
obj.put("type", "inPlace");
obj.put("text", commandsNode.textValue());
if (!dialectNode.isNull()) {
obj.put("dialect", dialectNode.textValue());
} else {
obj.putNull("dialect");
}
n.set("textSource", obj);
}
}

View File

@@ -8,6 +8,7 @@ import io.xpipe.app.hub.action.StoreActionCategory;
import io.xpipe.app.hub.comp.StoreCreationDialog;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.process.OsFileSystem;
import io.xpipe.app.process.ShellScript;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.FileOpener;
@@ -75,14 +76,20 @@ public class SimpleScriptQuickEditHubLeafProvider implements HubLeafProvider<Sim
return;
}
var inPlace = ref.getStore().getTextSource() instanceof ScriptTextSource.InPlace;
if (!inPlace) {
StoreCreationDialog.showEdit(ref.get());
return;
}
var script = ref.getStore();
var dialect = script.getMinimumDialect();
var ext = dialect != null ? dialect.getScriptFileEnding() : "sh";
var name = OsFileSystem.ofLocal().makeFileSystemCompatible(ref.get().getName());
FileOpener.openString(name + "." + ext, this, script.getCommands(), (s) -> {
FileOpener.openString(name + "." + ext, this, script.getTextSource().getText().getValue(), (s) -> {
DataStorage.get()
.updateEntryStore(
ref.get(), script.toBuilder().commands(s).build());
ref.get(), script.toBuilder().textSource(ScriptTextSource.InPlace.builder().dialect(dialect).text(ShellScript.of(s)).build()).build());
});
}
}

View File

@@ -29,37 +29,30 @@ import java.util.stream.Collectors;
@ToString(callSuper = true)
public class SimpleScriptStore extends ScriptStore implements SelfReferentialStore {
ShellDialect minimumDialect;
String commands;
ScriptTextSource textSource;
boolean initScript;
boolean shellScript;
boolean fileScript;
boolean runnableScript;
public String getCommands() {
return commands != null ? commands : "";
public ShellDialect getMinimumDialect() {
return textSource != null ? textSource.getDialect() : null;
}
public boolean isCompatible(ShellControl shellControl) {
var targetType = shellControl.getOriginalShellDialect();
return minimumDialect == null || minimumDialect.isCompatibleTo(targetType);
return getMinimumDialect() == null || getMinimumDialect().isCompatibleTo(targetType);
}
public boolean isCompatible(ShellDialect dialect) {
return minimumDialect == null || minimumDialect.isCompatibleTo(dialect);
return getMinimumDialect() == null || getMinimumDialect().isCompatibleTo(dialect);
}
private String assembleScript(ShellControl shellControl, boolean args) {
if (isCompatible(shellControl)) {
var shebang = getCommands().startsWith("#");
// Fix new lines and shebang
var fixedCommands = getCommands()
.lines()
.skip(shebang ? 1 : 0)
.collect(Collectors.joining(
shellControl.getShellDialect().getNewLine().getNewLineString()));
var raw = getTextSource().getText().withoutShebang();
var targetType = shellControl.getOriginalShellDialect();
var script = ScriptHelper.createExecScript(targetType, shellControl, fixedCommands);
var script = ScriptHelper.createExecScript(targetType, shellControl, raw);
return targetType.sourceScriptCommand(shellControl, script.toString()) + (args ? " "
+ targetType.getCatchAllVariable() : "");
}
@@ -82,6 +75,7 @@ public class SimpleScriptStore extends ScriptStore implements SelfReferentialSto
@Override
public void checkComplete() throws Throwable {
Validators.nonNull(textSource);
Validators.nonNull(group);
super.checkComplete();
if (!initScript && !shellScript && !fileScript && !runnableScript) {

View File

@@ -7,6 +7,7 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.*;
import io.xpipe.app.hub.comp.*;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.platform.OptionsChoiceBuilder;
import io.xpipe.app.platform.Validator;
import io.xpipe.app.process.OsFileSystem;
import io.xpipe.app.process.ShellDialect;
@@ -75,22 +76,11 @@ public class SimpleScriptStoreProvider implements EnabledParentStoreProvider, Da
public GuiDialog guiDialog(DataStoreEntry entry, Property<DataStore> store) {
SimpleScriptStore st = store.getValue().asNeeded();
var textSource = new SimpleObjectProperty<>(st.getTextSource());
var group = new SimpleObjectProperty<>(st.getGroup());
Property<ShellDialect> dialect = new SimpleObjectProperty<>(st.getMinimumDialect());
var others =
new SimpleListProperty<>(FXCollections.observableArrayList(new ArrayList<>(st.getEffectiveScripts())));
Property<String> commandProp = new SimpleObjectProperty<>(st.getCommands());
var others = new SimpleListProperty<>(FXCollections.observableArrayList(new ArrayList<>(st.getEffectiveScripts())));
var availableDialects = List.of(
ShellDialects.SH,
ShellDialects.BASH,
ShellDialects.ZSH,
ShellDialects.FISH,
ShellDialects.CMD,
ShellDialects.POWERSHELL,
ShellDialects.POWERSHELL_CORE);
Comp<?> choice =
new ShellDialectChoiceComp(availableDialects, dialect, ShellDialectChoiceComp.NullHandling.NULL_IS_ALL);
var textSourceChoice = OptionsChoiceBuilder.builder().property(textSource).available(ScriptTextSource.getClasses()).build();
var vals = List.of(0, 1, 2, 3);
var selectedStart = new ArrayList<Integer>();
@@ -134,20 +124,9 @@ public class SimpleScriptStoreProvider implements EnabledParentStoreProvider, Da
FXCollections.observableList(vals), name, selectedExecTypes, v -> false, () -> false);
return new OptionsBuilder()
.name("minimumShellDialect")
.description("minimumShellDialectDescription")
.documentationLink(DocumentationLink.SCRIPTING_COMPATIBILITY)
.addComp(choice, dialect)
.name("scriptContents")
.description("scriptContentsDescription")
.documentationLink(DocumentationLink.SCRIPTING_EDITING)
.addComp(
new IntegratedTextAreaComp(commandProp, false, "commands", Bindings.createStringBinding(() -> {
return dialect.getValue() != null
? dialect.getValue().getScriptFileEnding()
: "sh";
})),
commandProp)
.nameAndDescription("scriptSourceType")
.sub(textSourceChoice.build(), textSource)
.nonNull()
.nameAndDescription("executionType")
.documentationLink(DocumentationLink.SCRIPTING_TYPES)
.addComp(selectorComp, selectedExecTypes)
@@ -178,11 +157,10 @@ public class SimpleScriptStoreProvider implements EnabledParentStoreProvider, Da
.bind(
() -> {
return SimpleScriptStore.builder()
.textSource(textSource.get())
.group(group.get())
.minimumDialect(dialect.getValue())
.scripts(new ArrayList<>(others.get()))
.description(st.getDescription())
.commands(commandProp.getValue())
.initScript(selectedExecTypes.contains(0))
.runnableScript(selectedExecTypes.contains(1))
.fileScript(selectedExecTypes.contains(2))

View File

@@ -42,9 +42,7 @@ open module io.xpipe.ext.base {
RunBackgroundScriptActionProvider,
RunHubBatchScriptActionProvider,
RunHubScriptActionProvider,
RunTerminalScriptActionProvider,
ScriptSourceRefreshHubProvider,
ScriptSourceBrowseActionProvider,
RunTerminalScriptActionProvider, ScriptCollectionSourceRefreshHubProvider, ScriptCollectionSourceBrowseActionProvider,
SimpleScriptQuickEditHubLeafProvider,
StoreStartActionProvider,
StoreStopActionProvider,
@@ -60,8 +58,7 @@ open module io.xpipe.ext.base {
CustomServiceStoreProvider,
MappedServiceStoreProvider,
FixedServiceStoreProvider,
SimpleScriptStoreProvider,
ScriptSourceStoreProvider,
SimpleScriptStoreProvider, ScriptCollectionSourceStoreProvider,
DesktopApplicationStoreProvider,
LocalIdentityStoreProvider,
SyncedIdentityStoreProvider,

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1905,11 +1905,18 @@ scriptDirectory=Directory location
scriptDirectoryDescription=The local directory containing shell script files
scriptSourceUrl=Repository URL
scriptSourceUrlDescription=The URL to a remote git repository containing shell script files
scriptSourceType=Source type
scriptSourceTypeDescription=The type of source from where shell scripts should be loaded
scriptCollectionSourceType=Source type
scriptCollectionSourceTypeDescription=The type of source from where shell scripts should be loaded
gitRepository=Git repository
scriptSource.displayName=Script source
scriptSource.displayDescription=Automatically import shell script files from an existing source
scriptCollectionSource.displayName=Script source
scriptCollectionSource.displayDescription=Automatically import shell script files from an existing source
directorySource=Directory source
gitRepositorySource=Git repository source
refreshSource=Refresh source
scriptTextSourceUrl=Script URL
scriptTextSourceUrlDescription=The URL to retrieve the script file from
scriptSourceType=Script source
scriptSourceTypeDescription=From where to source the script
scriptSourceTypeInPlace=In-place
scriptSourceTypeUrl=URL
scriptSourceTypeSource=Source