diff --git a/app/build.gradle b/app/build.gradle index 4daf1db39..7a44db7c9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -132,7 +132,7 @@ application { run { systemProperty 'io.xpipe.app.mode', 'gui' - systemProperty 'io.xpipe.app.dataDir', "$projectDir/local3/" + systemProperty 'io.xpipe.app.dataDir', "$projectDir/local_stage/" systemProperty 'io.xpipe.app.writeLogs', "true" systemProperty 'io.xpipe.app.writeSysOut', "true" systemProperty 'io.xpipe.app.developerMode', "true" diff --git a/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java b/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java index 1f6a73ba5..1c65ee96b 100644 --- a/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java +++ b/app/src/main/java/io/xpipe/app/comp/AppLayoutComp.java @@ -80,8 +80,8 @@ public class AppLayoutComp extends Comp> { pane.setCenter(r); }); pane.setCenter(selected.getValue().comp().createRegion()); - pane.setPrefWidth(1200); - pane.setPrefHeight(700); + pane.setPrefWidth(1280); + pane.setPrefHeight(720); AppFont.normal(pane); return new SimpleCompStructure<>(pane); } diff --git a/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java b/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java index efacf93f7..f918477f5 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java @@ -5,16 +5,12 @@ import io.xpipe.extension.fxcomps.Comp; import io.xpipe.extension.fxcomps.CompStructure; import io.xpipe.extension.fxcomps.util.PlatformThread; import io.xpipe.extension.fxcomps.util.SimpleChangeListener; -import javafx.animation.Animation; -import javafx.animation.PauseTransition; import javafx.beans.property.Property; import javafx.beans.property.SimpleStringProperty; import javafx.event.EventHandler; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; -import javafx.scene.input.MouseEvent; import javafx.scene.layout.StackPane; -import javafx.util.Duration; import lombok.Builder; import lombok.Value; @@ -80,17 +76,6 @@ public class LazyTextFieldComp extends Comp { currentValue.setValue(newValue); }); - Animation delay = new PauseTransition(Duration.millis(800)); - delay.setOnFinished(e -> { - r.setDisable(false); - r.requestFocus(); - }); - sp.addEventFilter(MouseEvent.MOUSE_ENTERED, e -> { - delay.playFromStart(); - }); - sp.addEventFilter(MouseEvent.MOUSE_EXITED, e -> { - delay.stop(); - }); r.focusedProperty().addListener((c, o, n) -> { if (!n) { r.setDisable(true); diff --git a/app/src/main/java/io/xpipe/app/comp/base/ListBoxViewComp.java b/app/src/main/java/io/xpipe/app/comp/base/ListBoxViewComp.java new file mode 100644 index 000000000..71fa50c9d --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/base/ListBoxViewComp.java @@ -0,0 +1,77 @@ +package io.xpipe.app.comp.base; + +import io.xpipe.extension.fxcomps.Comp; +import io.xpipe.extension.fxcomps.CompStructure; +import io.xpipe.extension.fxcomps.SimpleCompStructure; +import io.xpipe.extension.fxcomps.util.BindingsHelper; +import io.xpipe.extension.fxcomps.util.PlatformThread; +import javafx.application.Platform; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +public class ListBoxViewComp extends Comp> { + + private final ObservableList shown; + private final ObservableList all; + private final Function> compFunction; + + public ListBoxViewComp( + ObservableList shown, ObservableList all, Function> compFunction) { + this.shown = PlatformThread.sync(shown); + this.all = PlatformThread.sync(all); + this.compFunction = compFunction; + } + + @Override + public CompStructure createBase() { + Map cache = new HashMap<>(); + + VBox listView = new VBox(); + listView.setFocusTraversable(false); + + refresh(listView, shown, cache, false); + listView.requestLayout(); + + shown.addListener((ListChangeListener) (c) -> { + refresh(listView, c.getList(), cache, true); + }); + + all.addListener((ListChangeListener) c -> { + cache.keySet().retainAll(c.getList()); + }); + + return new SimpleCompStructure<>(listView); + } + + private void refresh(VBox listView, List c, Map cache, boolean asynchronous) { + Runnable update = () -> { + var newShown = c.stream() + .map(v -> { + if (!cache.containsKey(v)) { + cache.put(v, compFunction.apply(v).createRegion()); + } + + return cache.get(v); + }) + .toList(); + + if (!listView.getChildren().equals(newShown)) { + BindingsHelper.setContent(listView.getChildren(), newShown); + listView.layout(); + } + }; + + if (asynchronous) { + Platform.runLater(update); + } else { + PlatformThread.runLaterIfNeeded(update); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java index 591d55c31..9c007a290 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryComp.java @@ -1,5 +1,6 @@ package io.xpipe.app.comp.storage.store; +import com.jfoenix.controls.JFXButton; import io.xpipe.app.comp.base.LazyTextFieldComp; import io.xpipe.app.comp.base.LoadingOverlayComp; import io.xpipe.app.core.AppFont; @@ -10,6 +11,7 @@ import io.xpipe.extension.I18n; import io.xpipe.extension.event.ErrorEvent; import io.xpipe.extension.fxcomps.Comp; import io.xpipe.extension.fxcomps.SimpleComp; +import io.xpipe.extension.fxcomps.SimpleCompStructure; import io.xpipe.extension.fxcomps.augment.GrowAugment; import io.xpipe.extension.fxcomps.augment.PopupMenuAugment; import io.xpipe.extension.fxcomps.impl.FancyTooltipAugment; @@ -100,6 +102,9 @@ public class StoreEntryComp extends SimpleComp { var imageComp = new PrettyImageComp(new SimpleStringProperty(img), 55, 45); var storeIcon = imageComp.createRegion(); storeIcon.getStyleClass().add("icon"); + if (entry.getState().getValue().isUsable()) { + new FancyTooltipAugment<>(new SimpleStringProperty(entry.getEntry().getProvider().getDisplayName())).augment(storeIcon); + } return storeIcon; } @@ -117,6 +122,7 @@ public class StoreEntryComp extends SimpleComp { var storeIcon = createIcon(); + grid.getColumnConstraints() .addAll( createShareConstraint(grid, STORE_TYPE_WIDTH), createShareConstraint(grid, NAME_WIDTH), @@ -133,7 +139,7 @@ public class StoreEntryComp extends SimpleComp { AppFont.small(size); AppFont.small(date); - grid.getStyleClass().add("store-entry-comp"); + grid.getStyleClass().add("store-entry-grid"); grid.setOnMouseClicked(event -> { if (entry.getEditable().get()) { @@ -143,7 +149,12 @@ public class StoreEntryComp extends SimpleComp { applyState(grid); - return grid; + var button = new JFXButton(); + button.setGraphic(grid); + GrowAugment.create(true, false).augment(new SimpleCompStructure<>(grid)); + button.getStyleClass().add("store-entry-comp"); + button.setMaxWidth(2000); + return button; } private Comp createButtonBar() { diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryListComp.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryListComp.java index 5ea1df5a0..a7405142a 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryListComp.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntryListComp.java @@ -4,7 +4,6 @@ import io.xpipe.app.comp.base.ListViewComp; import io.xpipe.app.comp.base.MultiContentComp; import io.xpipe.extension.fxcomps.Comp; import io.xpipe.extension.fxcomps.SimpleComp; -import io.xpipe.extension.fxcomps.augment.GrowAugment; import io.xpipe.extension.fxcomps.util.BindingsHelper; import javafx.beans.binding.Bindings; import javafx.beans.value.ObservableBooleanValue; @@ -15,14 +14,18 @@ import java.util.Map; public class StoreEntryListComp extends SimpleComp { private Comp createList() { + var topLevel = StoreEntrySection.createTopLevels(); + var filtered = BindingsHelper.filteredContentBinding( + topLevel, + StoreViewState.get().getFilterString().map(s -> (storeEntrySection -> storeEntrySection.shouldShow(s)))); var content = new ListViewComp<>( - StoreViewState.get().getShownEntries(), - StoreViewState.get().getAllEntries(), + filtered, + topLevel, null, - (StoreEntryWrapper e) -> { - return new StoreEntryComp(e).apply(GrowAugment.create(true, false)); + (StoreEntrySection e) -> { + return e.comp(true); }); - return content; + return content.styleClass("store-list-comp"); } @Override diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntrySection.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntrySection.java new file mode 100644 index 000000000..898fb55db --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreEntrySection.java @@ -0,0 +1,120 @@ +package io.xpipe.app.comp.storage.store; + +import io.xpipe.app.comp.base.ListBoxViewComp; +import io.xpipe.app.comp.storage.StorageFilter; +import io.xpipe.extension.fxcomps.Comp; +import io.xpipe.extension.fxcomps.augment.GrowAugment; +import io.xpipe.extension.fxcomps.impl.HorizontalComp; +import io.xpipe.extension.fxcomps.impl.VerticalComp; +import io.xpipe.extension.fxcomps.util.BindingsHelper; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.time.Instant; +import java.util.Comparator; +import java.util.List; + +public class StoreEntrySection implements StorageFilter.Filterable { + + public StoreEntrySection(StoreEntryWrapper entry, ObservableList children) { + this.entry = entry; + this.children = children; + } + + public static ObservableList createTopLevels() { + var topLevel = BindingsHelper.mappedContentBinding( + StoreViewState.get() + .getAllEntries() + .filtered(storeEntryWrapper -> + !storeEntryWrapper.getEntry().getState().isUsable() + || storeEntryWrapper + .getEntry() + .getProvider() + .getParent(storeEntryWrapper + .getEntry() + .getStore()) + == null), + storeEntryWrapper -> create(storeEntryWrapper)); + var ordered = BindingsHelper.orderedContentBinding( + topLevel, + Comparator.comparing(storeEntrySection -> + storeEntrySection.entry.lastAccessProperty().getValue()) + .reversed()); + return ordered; + } + + public static StoreEntrySection create(StoreEntryWrapper e) { + if (!e.getEntry().getState().isUsable()) { + return new StoreEntrySection(e, FXCollections.observableArrayList()); + } + + var children = BindingsHelper.mappedContentBinding( + StoreViewState.get() + .getAllEntries() + .filtered(other -> other.getEntry().getState().isUsable() + && e.getEntry() + .getStore() + .equals(other.getEntry() + .getProvider() + .getParent(other.getEntry().getStore()))), + entry1 -> create(entry1)); + var ordered = BindingsHelper.orderedContentBinding( + children, + Comparator.comparing(storeEntrySection -> + storeEntrySection.entry.lastAccessProperty().getValue()) + .reversed()); + return new StoreEntrySection(e, ordered); + } + + private final StoreEntryWrapper entry; + private final ObservableList children; + + public Comp comp(boolean top) { + var root = new StoreEntryComp(entry).apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS)); + var icon = Comp.of(() -> { + var padding = new FontIcon("mdal-arrow_forward_ios"); + padding.setIconSize(14); + var pain = new StackPane(padding); + pain.setMinWidth(20); + pain.setMaxHeight(20); + return pain; + }); + List> topEntryList = top ? List.of(root) : List.of(icon, root); + + if (children.size() == 0) { + return new HorizontalComp(topEntryList); + } + + var all = BindingsHelper.orderedContentBinding( + children, + Comparator.comparing(storeEntrySection -> + storeEntrySection.entry.lastAccessProperty().getValue())); + var shown = BindingsHelper.filteredContentBinding( + all, + StoreViewState.get().getFilterString().map(s -> (storeEntrySection -> storeEntrySection.shouldShow(s)))); + var content = new ListBoxViewComp<>(shown, all, (StoreEntrySection e) -> { + return e.comp(false).apply(GrowAugment.create(true, false)); + }) + .apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS)) + .apply(struc -> struc.get().backgroundProperty().set(Background.fill(Color.color(0, 0, 0, 0.01)))); + var spacer = Comp.of(() -> { + var padding = new Region(); + padding.setMinWidth(25); + padding.setMaxWidth(25); + return padding; + }); + return new VerticalComp(List.of( + new HorizontalComp(topEntryList), + new HorizontalComp(List.of(spacer, content)) + .apply(struc -> struc.get().setFillHeight(true)))); + } + + @Override + public boolean shouldShow(String filter) { + return entry.shouldShow(filter) + || children.stream().anyMatch(storeEntrySection -> storeEntrySection.shouldShow(filter)); + } +} diff --git a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreViewState.java b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreViewState.java index 5502991da..9084035f7 100644 --- a/app/src/main/java/io/xpipe/app/comp/storage/store/StoreViewState.java +++ b/app/src/main/java/io/xpipe/app/comp/storage/store/StoreViewState.java @@ -11,6 +11,7 @@ import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableBooleanValue; +import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -95,6 +96,10 @@ public class StoreViewState { return filter; } + public ObservableValue getFilterString() { + return filter.filterProperty(); + } + public ObservableList getAllEntries() { return allEntries; } diff --git a/app/src/main/java/io/xpipe/app/core/App.java b/app/src/main/java/io/xpipe/app/core/App.java index 17bc174e4..20ed9dab7 100644 --- a/app/src/main/java/io/xpipe/app/core/App.java +++ b/app/src/main/java/io/xpipe/app/core/App.java @@ -78,6 +78,14 @@ public class App extends Application { focus(); }); appWindow.show(); + + // For demo purposes + if (true) { + stage.setX(310); + stage.setY(178); + stage.setWidth(1300); + stage.setHeight(730); + } } public void focus() { diff --git a/app/src/main/java/io/xpipe/app/storage/DataSourceCollection.java b/app/src/main/java/io/xpipe/app/storage/DataSourceCollection.java index 80a518c21..e41b6c507 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataSourceCollection.java +++ b/app/src/main/java/io/xpipe/app/storage/DataSourceCollection.java @@ -60,6 +60,7 @@ public class DataSourceCollection extends StorageElement { var json = mapper.readTree(dir.resolve("collection.json").toFile()); var uuid = UUID.fromString(json.required("uuid").textValue()); var name = json.required("name").textValue(); + Objects.requireNonNull(name); var lastModified = Instant.parse(json.required("lastModified").textValue()); JavaType listType = mapper.getTypeFactory().constructCollectionType(ArrayList.class, UUID.class); diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorage.java b/app/src/main/java/io/xpipe/app/storage/DataStorage.java index c46144c71..c4fc7ccc8 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java @@ -82,7 +82,7 @@ public abstract class DataStorage { } public DataSourceCollection getInternalCollection() { - var found = sourceCollections.stream().filter(o -> o.getName().equals("Internal")).findAny(); + var found = sourceCollections.stream().filter(o -> o.getName() != null && o.getName().equals("Internal")).findAny(); if (found.isPresent()) { return found.get(); } diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorageWriter.java b/app/src/main/java/io/xpipe/app/storage/DataStorageWriter.java index 8adf1a473..5051b59d9 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorageWriter.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorageWriter.java @@ -17,16 +17,16 @@ public class DataStorageWriter { public static JsonNode storeToNode(DataStore store) { var mapper = JacksonMapper.newMapper(); var tree = mapper.valueToTree(store); - return replaceReferencesWithIds(store, tree); + return replaceReferencesWithIds(tree, true); } public static JsonNode sourceToNode(DataSource source) { var mapper = JacksonMapper.newMapper(); var tree = mapper.valueToTree(source); - return replaceReferencesWithIds(source, tree); + return replaceReferencesWithIds(tree, true); } - private static JsonNode replaceReferencesWithIds(Object root, JsonNode node) { + private static JsonNode replaceReferencesWithIds(JsonNode node, boolean isRoot) { var mapper = JacksonMapper.newMapper(); node = replaceReferencesWithIds( @@ -38,7 +38,7 @@ public class DataStorageWriter { try { var store = mapper.treeToValue(possibleReference, DataStore.class); - if (root == null || !root.equals(store)) { + if (!isRoot) { var found = DataStorage.get().getEntryByStore(store); return found.map(dataSourceEntry -> dataSourceEntry.getUuid()); } @@ -46,14 +46,14 @@ public class DataStorageWriter { } return Optional.empty(); }, - "storeId"); + "storeId", isRoot); node = replaceReferencesWithIds( node, possibleReference -> { try { var source = mapper.treeToValue(possibleReference, DataSource.class); - if (root == null || !root.equals(source)) { + if (!isRoot) { var found = DataStorage.get().getEntryBySource(source); return found.map(dataSourceEntry -> dataSourceEntry.getUuid()); } @@ -61,13 +61,13 @@ public class DataStorageWriter { } return Optional.empty(); }, - "sourceId"); + "sourceId", isRoot); return node; } private static JsonNode replaceReferencesWithIds( - JsonNode node, Function> function, String key) { + JsonNode node, Function> function, String key, boolean isRoot) { if (!node.isObject()) { return node; } @@ -80,7 +80,7 @@ public class DataStorageWriter { var replacement = JsonNodeFactory.instance.objectNode(); node.fields().forEachRemaining(stringJsonNodeEntry -> { - var resolved = replaceReferencesWithIds(null, stringJsonNodeEntry.getValue()); + var resolved = replaceReferencesWithIds(stringJsonNodeEntry.getValue(), false); replacement.set(stringJsonNodeEntry.getKey(), resolved); }); return replacement; diff --git a/app/src/main/resources/io/xpipe/app/resources/style/storage/storage-group-list-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/storage/storage-group-list-comp.css index 943d32684..fdba14673 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/storage/storage-group-list-comp.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/storage/storage-group-list-comp.css @@ -75,3 +75,7 @@ -fx-padding: 0; -fx-focus-color: transparent; } + +.store-list-comp .list-cell { +-fx-padding: 0; +} diff --git a/app/src/main/resources/io/xpipe/app/resources/style/storage/store-entry-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/storage/store-entry-comp.css index f81d0e87e..ba973769f 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/storage/store-entry-comp.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/storage/store-entry-comp.css @@ -1,27 +1,30 @@ -.store-entry-comp .date, .store-entry-comp .summary { +.store-entry-grid .date, .store-entry-grid .summary { -fx-text-fill: -xp-text-light; } -.store-entry-comp:failed .jfx-text-field { +.store-entry-grid:failed .jfx-text-field { -fx-text-fill: red; } -.store-entry-comp:incomplete .jfx-text-field { +.store-entry-grid:incomplete .jfx-text-field { -fx-text-fill: gray; } -.store-entry-comp:incomplete .summary { +.store-entry-grid:incomplete .summary { -fx-text-fill: gray; } -.store-entry-comp:incomplete .information { +.store-entry-grid:incomplete .information { -fx-text-fill: gray; } -.store-entry-comp:incomplete .date { +.store-entry-grid:incomplete .date { -fx-text-fill: gray; } -.store-entry-comp:incomplete .icon { +.store-entry-grid:incomplete .icon { -fx-opacity: 0.5; } +.store-entry-comp { +-fx-padding: 6px; +} diff --git a/core/src/main/java/io/xpipe/core/process/ShellProcessControl.java b/core/src/main/java/io/xpipe/core/process/ShellProcessControl.java index cf05507c5..24074cbf2 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellProcessControl.java +++ b/core/src/main/java/io/xpipe/core/process/ShellProcessControl.java @@ -11,11 +11,9 @@ import java.util.function.Predicate; public interface ShellProcessControl extends ProcessControl { - default String prepareTerminalOpen() throws Exception { - return prepareTerminalOpen(null); - } + String prepareTerminalOpen() throws Exception; - String prepareTerminalOpen(String content) throws Exception; + String prepareIntermediateTerminalOpen(String content) throws Exception; default String executeStringSimpleCommand(String command) throws Exception { try (CommandProcessControl c = command(command).start()) { @@ -35,7 +33,7 @@ public interface ShellProcessControl extends ProcessControl { } } - default void executeSimpleCommand(String command,String failMessage) throws Exception { + default void executeSimpleCommand(String command, String failMessage) throws Exception { try (CommandProcessControl c = command(command).start()) { c.discardOrThrow(); } catch (ProcessOutputException out) { @@ -60,12 +58,14 @@ public interface ShellProcessControl extends ProcessControl { ShellProcessControl elevation(SecretValue value); + ShellProcessControl initWith(List cmds); + SecretValue getElevationPassword(); default ShellProcessControl subShell(@NonNull ShellType type) { return subShell(p -> type.getNormalOpenCommand(), (shellProcessControl, s) -> { - return s == null ? type.getNormalOpenCommand() : type.executeCommandWithShell(s); - }) + return s == null ? type.getNormalOpenCommand() : type.executeCommandWithShell(s); + }) .elevation(getElevationPassword()); } @@ -80,10 +80,9 @@ public interface ShellProcessControl extends ProcessControl { ShellProcessControl subShell( @NonNull Function command, - BiFunction terminalCommand - ); + BiFunction terminalCommand); - void executeCommand(String command) throws Exception; + void executeLine(String command) throws Exception; @Override ShellProcessControl start() throws Exception; @@ -91,8 +90,7 @@ public interface ShellProcessControl extends ProcessControl { CommandProcessControl command(Function command); CommandProcessControl command( - Function command, Function terminalCommand - ); + Function command, Function terminalCommand); default CommandProcessControl command(String command) { return command(shellProcessControl -> command); diff --git a/core/src/main/java/io/xpipe/core/process/ShellType.java b/core/src/main/java/io/xpipe/core/process/ShellType.java index d24c0a2a7..0a355975f 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellType.java +++ b/core/src/main/java/io/xpipe/core/process/ShellType.java @@ -3,7 +3,6 @@ package io.xpipe.core.process; import com.fasterxml.jackson.annotation.JsonTypeInfo; import io.xpipe.core.charsetter.NewLine; -import java.io.IOException; import java.nio.charset.Charset; import java.util.List; import java.util.Map; @@ -55,7 +54,7 @@ public interface ShellType { return getEchoCommand(s, false); } - String getSetVariableCommand(String variable, String value); + String getSetEnvironmentVariableCommand(String variable, String value); String getEchoCommand(String s, boolean toErrorStream); @@ -67,8 +66,14 @@ public interface ShellType { String getPrintVariableCommand(String prefix, String name); + default String getPrintEnvironmentVariableCommand(String name) { + return getPrintVariableCommand(name); + } + String getNormalOpenCommand(); + String getInitFileOpenCommand(String file); + String executeCommandWithShell(String cmd); List executeCommandListWithShell(String cmd); @@ -79,7 +84,7 @@ public interface ShellType { String getStreamFileWriteCommand(String file); - String getSimpleFileWriteCommand(String content, String file); + String getTextFileWriteCommand(String content, String file); String getFileDeleteCommand(String file); diff --git a/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java b/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java index f46e6139d..4f963dbf6 100644 --- a/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java +++ b/core/src/main/java/io/xpipe/core/util/XPipeInstallation.java @@ -79,9 +79,9 @@ public class XPipeInstallation { public static Path getLocalDynamicLibraryDirectory() { Path path = getLocalInstallationBasePath(); if (OsType.getLocal().equals(OsType.WINDOWS)) { - return path.resolve("runtime").resolve("bin"); + return path.resolve("app").resolve("runtime").resolve("bin"); } else if (OsType.getLocal().equals(OsType.LINUX)) { - return path.resolve("lib").resolve("runtime").resolve("lib"); + return path.resolve("app").resolve("lib").resolve("runtime").resolve("lib"); } else { return path.resolve("Contents") .resolve("runtime") @@ -95,7 +95,7 @@ public class XPipeInstallation { Path path = getLocalInstallationBasePath(); return OsType.getLocal().equals(OsType.MAC) ? path.resolve("Contents").resolve("Resources").resolve("extensions") - : path.resolve("extensions"); + : path.resolve("app").resolve("extensions"); } private static Path getLocalInstallationBasePathForJavaExecutable(Path executable) { @@ -108,9 +108,9 @@ public class XPipeInstallation { .getParent() .getParent(); } else if (OsType.getLocal().equals(OsType.LINUX)) { - return executable.getParent().getParent().getParent().getParent(); + return executable.getParent().getParent().getParent().getParent().getParent(); } else { - return executable.getParent().getParent().getParent(); + return executable.getParent().getParent().getParent().getParent(); } } @@ -118,9 +118,9 @@ public class XPipeInstallation { if (OsType.getLocal().equals(OsType.MAC)) { return executable.getParent().getParent().getParent(); } else if (OsType.getLocal().equals(OsType.LINUX)) { - return executable.getParent().getParent(); + return executable.getParent().getParent().getParent(); } else { - return executable.getParent(); + return executable.getParent().getParent(); } } diff --git a/dist/cli.gradle b/dist/cli.gradle index edf1bbd3d..b48b44a04 100644 --- a/dist/cli.gradle +++ b/dist/cli.gradle @@ -3,8 +3,6 @@ import java.nio.file.Paths import java.nio.file.StandardCopyOption def distDir = "$buildDir/dist" -def windows = org.gradle.internal.os.OperatingSystem.current().isWindows(); - apply plugin: 'org.asciidoctor.jvm.convert' asciidoctor { @@ -29,21 +27,37 @@ task copyCompletion(type: Copy) { into "$distDir/cli" } +task setupMusl(type: Exec) { + commandLine "${project(':cli').projectDir.toString()}/musl-setup.sh", project(':cli').projectDir.toString() +} + task buildCli(type: DefaultTask) { - if (!org.gradle.internal.os.OperatingSystem.current().isWindows()) { + if (org.gradle.internal.os.OperatingSystem.current().isLinux()) { + dependsOn(setupMusl) + project(':cli').getTasksByName('nativeCompile', true).forEach(v->v.mustRunAfter(setupMusl)) + } + + if (org.gradle.internal.os.OperatingSystem.current().isMacOsX()) { dependsOn(project(':cli').getTasksByName('nativeCompile', true)) } doLast { - if (windows) { + if (rootProject.os.isWindows()) { exec { executable project(':cli').projectDir.toString() + '/native-build.bat' environment System.getenv() } } + if (rootProject.os.isLinux()) { + exec { + commandLine project(':cli').projectDir.toString() + '/native-build-musl.sh', project(':cli').projectDir.toString() + environment System.getenv() + } + } + Files.createDirectories(Paths.get(distDir, 'cli')) - def ending = windows ? ".exe" : "" + def ending = rootProject.os.isWindows() ? ".exe" : "" var outputFile = Paths.get(project(':cli').buildDir.toString() + "/native/nativeCompile/xpipe$ending") if (!Files.exists(outputFile)) { throw new IOException("Cli output file does not exist") diff --git a/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java b/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java index f1f1f4eb8..e91c63ae4 100644 --- a/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java +++ b/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java @@ -39,6 +39,10 @@ public interface DataStoreProvider { throw new ExtensionException("Provider " + getId() + " has no set category"); } + default DataStore getParent(DataStore store) { + return null; + } + default GuiDialog guiDialog(Property store) { return null; } diff --git a/extension/src/main/java/io/xpipe/extension/event/TrackEvent.java b/extension/src/main/java/io/xpipe/extension/event/TrackEvent.java index c91a5adc9..14c3307eb 100644 --- a/extension/src/main/java/io/xpipe/extension/event/TrackEvent.java +++ b/extension/src/main/java/io/xpipe/extension/event/TrackEvent.java @@ -72,6 +72,10 @@ public class TrackEvent { return builder().type("debug").message(message); } + public static TrackEventBuilder withDebug(String cat, String message) { + return builder().category(cat).type("debug").message(message); + } + public static void debug(String cat, String message) { builder().category(cat).type("debug").message(message).build().handle(); } diff --git a/extension/src/main/java/io/xpipe/extension/fxcomps/util/BindingsHelper.java b/extension/src/main/java/io/xpipe/extension/fxcomps/util/BindingsHelper.java index 757b34874..465746c27 100644 --- a/extension/src/main/java/io/xpipe/extension/fxcomps/util/BindingsHelper.java +++ b/extension/src/main/java/io/xpipe/extension/fxcomps/util/BindingsHelper.java @@ -1,6 +1,8 @@ package io.xpipe.extension.fxcomps.util; import javafx.beans.binding.Binding; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; @@ -8,6 +10,7 @@ import java.lang.ref.WeakReference; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; +import java.util.function.Predicate; public class BindingsHelper { @@ -44,6 +47,45 @@ public class BindingsHelper { }); } + public static ObservableList mappedContentBinding(ObservableList l2, Function map) { + ObservableList l1 = FXCollections.observableList(new ArrayList<>()); + Runnable runnable = () -> { + setContent(l1, l2.stream().map(map).toList()); + }; + runnable.run(); + l2.addListener((ListChangeListener) c -> { + runnable.run(); + }); + return l1; + } + + public static ObservableList orderedContentBinding(ObservableList l2, Comparator comp) { + ObservableList l1 = FXCollections.observableList(new ArrayList<>()); + Runnable runnable = () -> { + setContent(l1, l2.stream().sorted(comp).toList()); + }; + runnable.run(); + l2.addListener((ListChangeListener) c -> { + runnable.run(); + }); + return l1; + } + + public static ObservableList filteredContentBinding(ObservableList l2, ObservableValue> predicate) { + ObservableList l1 = FXCollections.observableList(new ArrayList<>()); + Runnable runnable = () -> { + setContent(l1, l2.stream().filter(predicate.getValue()).toList()); + }; + runnable.run(); + l2.addListener((ListChangeListener) c -> { + runnable.run(); + }); + predicate.addListener((c,o,n) -> { + runnable.run(); + }); + return l1; + } + public static void setContent(ObservableList toSet, List newList) { if (toSet.equals(newList)) { return; diff --git a/extension/src/main/java/io/xpipe/extension/util/DataStoreFormatter.java b/extension/src/main/java/io/xpipe/extension/util/DataStoreFormatter.java index 87919a1c7..ed2c72fde 100644 --- a/extension/src/main/java/io/xpipe/extension/util/DataStoreFormatter.java +++ b/extension/src/main/java/io/xpipe/extension/util/DataStoreFormatter.java @@ -40,8 +40,8 @@ public class DataStoreFormatter { return func.apply(length); } - var fileString = func.apply(length - atString.length() - 4); - return String.format("%s -> %s", atString, fileString); + var fileString = func.apply(length - atString.length() - 3); + return String.format("%s > %s", atString, fileString); } public static String toName(DataStore input) { @@ -96,6 +96,17 @@ public class DataStoreFormatter { ); } + if (input.endsWith(".compute.amazonaws.com")) { + var split = input.split("\\."); + var name = split[0]; + var region = split[1]; + var lengthShare = (length - 3) / 2; + return String.format( + "%s.%s", + DataStoreFormatter.cut(name, lengthShare), DataStoreFormatter.cut(region, length - lengthShare) + ); + } + return cut(input, length); } } diff --git a/extension/src/main/java/io/xpipe/extension/util/ScriptHelper.java b/extension/src/main/java/io/xpipe/extension/util/ScriptHelper.java index 07f6e11ef..cd857ea62 100644 --- a/extension/src/main/java/io/xpipe/extension/util/ScriptHelper.java +++ b/extension/src/main/java/io/xpipe/extension/util/ScriptHelper.java @@ -1,21 +1,24 @@ package io.xpipe.extension.util; import io.xpipe.core.impl.FileNames; +import io.xpipe.core.process.OsType; import io.xpipe.core.process.ShellProcessControl; import io.xpipe.core.process.ShellType; import io.xpipe.core.store.ShellStore; import io.xpipe.core.util.SecretValue; -import io.xpipe.core.util.XPipeSession; import io.xpipe.core.util.XPipeTempDirectory; import io.xpipe.extension.event.TrackEvent; import lombok.SneakyThrows; -import java.util.Objects; +import java.util.List; +import java.util.Random; public class ScriptHelper { public static int getConnectionHash(String command) { - return Math.abs(Objects.hash(command, XPipeSession.get().getSystemSessionId())); + // This deterministic approach can cause permission problems when two different users execute the same command on a system + return new Random().nextInt(Integer.MAX_VALUE); + //return Math.abs(Objects.hash(command, XPipeSession.get().getSystemSessionId())); } @SneakyThrows @@ -25,6 +28,38 @@ public class ScriptHelper { } } + public static String constructOpenWithInitScriptCommand(ShellProcessControl processControl, List init, String toExecuteInShell) { + ShellType t = processControl.getShellType(); + if (init.size() == 0 && toExecuteInShell == null) { + return t.getNormalOpenCommand(); + } + + String nl = t.getNewLine().getNewLineString(); + var content = String.join(nl, init) + + nl; + + if (processControl.getOsType().equals(OsType.LINUX) + || processControl.getOsType().equals(OsType.MAC)) { + content = "if [ -f ~/.bashrc ]; then . ~/.bashrc; fi\n" + content; + } + + if (toExecuteInShell != null) { + content += toExecuteInShell + nl; + content += t.getExitCommand() + nl; + } + + var initFile = createExecScript(processControl, content, true); + return t.getInitFileOpenCommand(initFile); + } + + public static String prepend(ShellProcessControl processControl, List init, String commands) { + var prefix = init != null && init.size() > 0 + ? String.join(processControl.getShellType().getNewLine().getNewLineString(), init) + + processControl.getShellType().getNewLine().getNewLineString() + : ""; + return prefix + commands; + } + @SneakyThrows public static String createExecScript(ShellProcessControl processControl, String content, boolean restart) { var fileName = "exec-" + getConnectionHash(content); @@ -35,7 +70,8 @@ public class ScriptHelper { } @SneakyThrows - private static String createExecScript(ShellProcessControl processControl, String file, String content, boolean restart) { + private static String createExecScript( + ShellProcessControl processControl, String file, String content, boolean restart) { ShellType type = processControl.getShellType(); content = type.prepareScriptContent(content); @@ -49,32 +85,19 @@ public class ScriptHelper { .handle(); processControl.executeSimpleCommand(type.getFileTouchCommand(file), "Failed to create script " + file); - processControl.executeSimpleCommand(type.getMakeExecutableCommand(file), "Failed to make script " + file + " executable"); - - if (!content.contains("\n")) { - processControl.executeSimpleCommand(type.getSimpleFileWriteCommand(content, file)); - return file; - } - - try (var c = processControl.command(type.getStreamFileWriteCommand(file)).start()) { - c.discardOut(); - c.discardErr(); - c.getStdin().write(content.getBytes(processControl.getCharset())); - c.closeStdin(); - } - - if (restart) { - processControl.restart(); - } + processControl.executeSimpleCommand( + type.getMakeExecutableCommand(file), "Failed to make script " + file + " executable"); + processControl.executeSimpleCommand(type.getTextFileWriteCommand(content, file)); return file; } @SneakyThrows - public static String createAskPassScript(SecretValue pass, ShellProcessControl parent, ShellType type, boolean restart) { + public static String createAskPassScript( + SecretValue pass, ShellProcessControl parent, ShellType type, boolean restart) { var content = type.getScriptEchoCommand(pass.getSecretValue()); var temp = XPipeTempDirectory.get(parent); var file = FileNames.join(temp, "askpass-" + getConnectionHash(content) + "." + type.getScriptFileEnding()); - return createExecScript(parent,file, content, restart); + return createExecScript(parent, file, content, restart); } }