[stage] [noannounce]

This commit is contained in:
crschnick 2023-07-29 12:36:04 +00:00
parent 9e085afb46
commit 0b31eed2a5
135 changed files with 6046 additions and 895 deletions

View file

@ -1,7 +1,7 @@
package io.xpipe.api;
import io.xpipe.api.impl.DataSourceImpl;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataStoreId;
import io.xpipe.core.source.DataSourceReference;
import io.xpipe.core.source.DataSourceType;
import io.xpipe.core.store.DataStore;
@ -96,16 +96,16 @@ public interface DataSource {
}
/**
* Wrapper for {@link #create(DataSourceId, String, InputStream)} that creates an anonymous data source.
* Wrapper for {@link #create(DataStoreId, String, InputStream)} that creates an anonymous data source.
*/
static DataSource createAnonymous(String type, Path path) {
return create(null, type, path);
}
/**
* Wrapper for {@link #create(DataSourceId, String, InputStream)}.
* Wrapper for {@link #create(DataStoreId, String, InputStream)}.
*/
static DataSource create(DataSourceId id, String type, Path path) {
static DataSource create(DataStoreId id, String type, Path path) {
try (var in = Files.newInputStream(path)) {
return create(id, type, in);
} catch (IOException e) {
@ -114,16 +114,16 @@ public interface DataSource {
}
/**
* Wrapper for {@link #create(DataSourceId, String, InputStream)} that creates an anonymous data source.
* Wrapper for {@link #create(DataStoreId, String, InputStream)} that creates an anonymous data source.
*/
static DataSource createAnonymous(String type, URL url) {
return create(null, type, url);
}
/**
* Wrapper for {@link #create(DataSourceId, String, InputStream)}.
* Wrapper for {@link #create(DataStoreId, String, InputStream)}.
*/
static DataSource create(DataSourceId id, String type, URL url) {
static DataSource create(DataStoreId id, String type, URL url) {
try (var in = url.openStream()) {
return create(id, type, in);
} catch (IOException e) {
@ -132,7 +132,7 @@ public interface DataSource {
}
/**
* Wrapper for {@link #create(DataSourceId, String, InputStream)} that creates an anonymous data source.
* Wrapper for {@link #create(DataStoreId, String, InputStream)} that creates an anonymous data source.
*/
static DataSource createAnonymous(String type, InputStream in) {
return create(null, type, in);
@ -146,7 +146,7 @@ public interface DataSource {
* @param in the input stream to read
* @return a {@link DataSource} instances that can be used to access the underlying data
*/
static DataSource create(DataSourceId id, String type, InputStream in) {
static DataSource create(DataStoreId id, String type, InputStream in) {
return DataSourceImpl.create(id, type, in);
}
@ -156,7 +156,7 @@ public interface DataSource {
* @param id the data source id
* @return a {@link DataSource} instances that can be used to access the underlying data
*/
static DataSource create(DataSourceId id, io.xpipe.core.source.DataSource<?> source) {
static DataSource create(DataStoreId id, io.xpipe.core.source.DataSource<?> source) {
return DataSourceImpl.create(id, source);
}
@ -169,7 +169,7 @@ public interface DataSource {
* @param in the data store to add
* @return a {@link DataSource} instances that can be used to access the underlying data
*/
static DataSource create(DataSourceId id, String type, DataStore in) {
static DataSource create(DataStoreId id, String type, DataStore in) {
return DataSourceImpl.create(id, type, in);
}
@ -182,7 +182,7 @@ public interface DataSource {
/**
* Returns the id of this data source.
*/
DataSourceId getId();
DataStoreId getId();
/**
* Returns the type of this data source.

View file

@ -4,14 +4,14 @@ import io.xpipe.api.impl.DataTableAccumulatorImpl;
import io.xpipe.core.data.node.DataStructureNode;
import io.xpipe.core.data.node.DataStructureNodeAcceptor;
import io.xpipe.core.data.type.TupleType;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataStoreId;
/**
* An accumulator for table data.
* <p>
* This class can be used to construct new table data sources by
* accumulating the rows using {@link #add(DataStructureNode)} or {@link #acceptor()} and then calling
* {@link #finish(DataSourceId)} to complete the construction process and create a new data source.
* {@link #finish(DataStoreId)} to complete the construction process and create a new data source.
*/
public interface DataTableAccumulator {
@ -20,10 +20,10 @@ public interface DataTableAccumulator {
}
/**
* Wrapper for {@link #finish(DataSourceId)}.
* Wrapper for {@link #finish(DataStoreId)}.
*/
default DataTable finish(String id) {
return finish(DataSourceId.fromString(id));
return finish(DataStoreId.fromString(id));
}
/**
@ -31,7 +31,7 @@ public interface DataTableAccumulator {
*
* @param id the data source id to assign
*/
DataTable finish(DataSourceId id);
DataTable finish(DataStoreId id);
/**
* Adds a row to the table.

View file

@ -2,7 +2,7 @@ package io.xpipe.api.impl;
import io.xpipe.api.DataRaw;
import io.xpipe.api.DataSourceConfig;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataStoreId;
import io.xpipe.core.source.DataSourceType;
import java.io.InputStream;
@ -10,7 +10,7 @@ import java.io.InputStream;
public class DataRawImpl extends DataSourceImpl implements DataRaw {
public DataRawImpl(
DataSourceId sourceId, DataSourceConfig sourceConfig, io.xpipe.core.source.DataSource<?> internalSource) {
DataStoreId sourceId, DataSourceConfig sourceConfig, io.xpipe.core.source.DataSource<?> internalSource) {
super(sourceId, sourceConfig, internalSource);
}

View file

@ -4,7 +4,7 @@ import io.xpipe.api.DataSource;
import io.xpipe.api.DataSourceConfig;
import io.xpipe.api.connector.XPipeApiConnection;
import io.xpipe.beacon.exchange.*;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataStoreId;
import io.xpipe.core.source.DataSourceReference;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.StreamDataStore;
@ -13,12 +13,12 @@ import java.io.InputStream;
public abstract class DataSourceImpl implements DataSource {
private final DataSourceId sourceId;
private final DataStoreId sourceId;
private final DataSourceConfig config;
private final io.xpipe.core.source.DataSource<?> internalSource;
public DataSourceImpl(
DataSourceId sourceId, DataSourceConfig config, io.xpipe.core.source.DataSource<?> internalSource) {
DataStoreId sourceId, DataSourceConfig config, io.xpipe.core.source.DataSource<?> internalSource) {
this.sourceId = sourceId;
this.config = config;
this.internalSource = internalSource;
@ -48,7 +48,7 @@ public abstract class DataSourceImpl implements DataSource {
});
}
public static DataSource create(DataSourceId id, io.xpipe.core.source.DataSource<?> source) {
public static DataSource create(DataStoreId id, io.xpipe.core.source.DataSource<?> source) {
var startReq =
AddSourceExchange.Request.builder().source(source).target(id).build();
var returnedId = XPipeApiConnection.execute(con -> {
@ -60,7 +60,7 @@ public abstract class DataSourceImpl implements DataSource {
return get(ref);
}
public static DataSource create(DataSourceId id, String type, DataStore store) {
public static DataSource create(DataStoreId id, String type, DataStore store) {
if (store instanceof StreamDataStore s && s.isContentExclusivelyAccessible()) {
store = XPipeApiConnection.execute(con -> {
var internal = con.createInternalStreamStore();
@ -94,7 +94,7 @@ public abstract class DataSourceImpl implements DataSource {
return get(ref);
}
public static DataSource create(DataSourceId id, String type, InputStream in) {
public static DataSource create(DataStoreId id, String type, InputStream in) {
var store = XPipeApiConnection.execute(con -> {
var internal = con.createInternalStreamStore();
var req = WriteStreamExchange.Request.builder()
@ -152,7 +152,7 @@ public abstract class DataSourceImpl implements DataSource {
}
@Override
public DataSourceId getId() {
public DataStoreId getId() {
return sourceId;
}

View file

@ -3,13 +3,13 @@ package io.xpipe.api.impl;
import io.xpipe.api.DataSourceConfig;
import io.xpipe.api.DataStructure;
import io.xpipe.core.data.node.DataStructureNode;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataStoreId;
import io.xpipe.core.source.DataSourceType;
public class DataStructureImpl extends DataSourceImpl implements DataStructure {
DataStructureImpl(
DataSourceId sourceId, DataSourceConfig sourceConfig, io.xpipe.core.source.DataSource<?> internalSource) {
DataStoreId sourceId, DataSourceConfig sourceConfig, io.xpipe.core.source.DataSource<?> internalSource) {
super(sourceId, sourceConfig, internalSource);
}

View file

@ -16,7 +16,7 @@ import io.xpipe.core.data.node.TupleNode;
import io.xpipe.core.data.type.TupleType;
import io.xpipe.core.data.typed.TypedDataStreamWriter;
import io.xpipe.core.impl.InternalStreamStore;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataStoreId;
import io.xpipe.core.source.DataSourceReference;
import java.io.IOException;
@ -51,7 +51,7 @@ public class DataTableAccumulatorImpl implements DataTableAccumulator {
}
@Override
public synchronized DataTable finish(DataSourceId id) {
public synchronized DataTable finish(DataStoreId id) {
try {
bodyOutput.close();
} catch (IOException e) {

View file

@ -5,7 +5,7 @@ import io.xpipe.api.DataTable;
import io.xpipe.core.data.node.ArrayNode;
import io.xpipe.core.data.node.DataStructureNode;
import io.xpipe.core.data.node.TupleNode;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataStoreId;
import io.xpipe.core.source.DataSourceType;
import java.util.ArrayList;
@ -15,7 +15,7 @@ import java.util.stream.Stream;
public class DataTableImpl extends DataSourceImpl implements DataTable {
DataTableImpl(DataSourceId id, DataSourceConfig sourceConfig, io.xpipe.core.source.DataSource<?> internalSource) {
DataTableImpl(DataStoreId id, DataSourceConfig sourceConfig, io.xpipe.core.source.DataSource<?> internalSource) {
super(id, sourceConfig, internalSource);
}

View file

@ -2,7 +2,7 @@ package io.xpipe.api.impl;
import io.xpipe.api.DataSourceConfig;
import io.xpipe.api.DataText;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataStoreId;
import io.xpipe.core.source.DataSourceType;
import java.util.List;
@ -12,7 +12,7 @@ import java.util.stream.Stream;
public class DataTextImpl extends DataSourceImpl implements DataText {
DataTextImpl(
DataSourceId sourceId, DataSourceConfig sourceConfig, io.xpipe.core.source.DataSource<?> internalSource) {
DataStoreId sourceId, DataSourceConfig sourceConfig, io.xpipe.core.source.DataSource<?> internalSource) {
super(sourceId, sourceConfig, internalSource);
}

View file

@ -1,7 +1,7 @@
package io.xpipe.api.test;
import io.xpipe.api.DataSource;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataStoreId;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@ -10,7 +10,7 @@ public class DataTableTest extends ApiTest {
@BeforeAll
public static void setupStorage() throws Exception {
DataSource.create(
DataSourceId.fromString(":usernames"), "csv", DataTableTest.class.getResource("username.csv"));
DataStoreId.fromString(":usernames"), "csv", DataTableTest.class.getResource("username.csv"));
}
@Test

View file

@ -156,11 +156,12 @@ run {
// systemProperty "io.xpipe.beacon.localProxy", "true"
systemProperties System.getProperties()
systemProperty 'java.library.path', "./lib"
workingDir = rootDir
}
task runAttachedDebugger(type: JavaExec) {
workingDir = rootDir
classpath = run.classpath
mainModule = 'io.xpipe.app'
mainClass = 'io.xpipe.app.Main'

View file

@ -4,6 +4,7 @@ import atlantafx.base.controls.Breadcrumbs;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.impl.FileNames;
import javafx.scene.Node;
import javafx.scene.control.Button;
@ -80,7 +81,9 @@ public class BrowserBreadcrumbBar extends SimpleComp {
}
breadcrumbs.selectedCrumbProperty().addListener((obs, old, val) -> {
model.cd(val != null ? val.getValue() : null);
ThreadHelper.runFailableAsync(() -> {
model.cdSync(val != null ? val.getValue() : null);
});
});
return breadcrumbs;

View file

@ -200,7 +200,7 @@ public class BrowserFileListCompEntry {
return;
}
model.getFileSystemModel().cd(item.getRawFileEntry().getPath());
model.getFileSystemModel().cdSync(item.getRawFileEntry().getPath());
}
};
DROP_TIMER.schedule(activeTask, 1000);

View file

@ -128,7 +128,7 @@ public final class BrowserFileListModel {
}
if (entry.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {
fileSystemModel.cd(entry.getRawFileEntry().resolved().getPath());
fileSystemModel.cdSync(entry.getRawFileEntry().resolved().getPath());
}
}
}

View file

@ -31,7 +31,7 @@ public class BrowserFileOverviewComp extends SimpleComp {
var icon = BrowserIcons.createIcon(entry);
var l = new Button(entry.getPath(), icon.createRegion());
l.setOnAction(event -> {
model.cd(entry.getPath());
model.cdSync(entry.getPath());
event.consume();
});
l.setAlignment(Pos.CENTER_LEFT);

View file

@ -11,6 +11,8 @@ import io.xpipe.app.fxcomps.impl.PrettyImageComp;
import io.xpipe.app.fxcomps.impl.StackComp;
import io.xpipe.app.fxcomps.impl.TextFieldComp;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.css.PseudoClass;
@ -42,8 +44,10 @@ public class BrowserNavBar extends SimpleComp {
path.set(newValue);
});
path.addListener((observable, oldValue, newValue) -> {
var changed = model.cdOrRetry(newValue, true);
changed.ifPresent(path::set);
ThreadHelper.runFailableAsync(() -> {
var changed = model.cdSyncOrRetry(newValue, true);
changed.ifPresent(s -> Platform.runLater(() -> path.set(s)));
});
});
var pathBar = new TextFieldComp(path, true)

View file

@ -57,6 +57,10 @@ public class BrowserWelcomeComp extends SimpleComp {
return;
}
if (!entry.get().getState().isUsable()) {
return;
}
var graphic =
entry.get().getProvider().getDisplayIconFileName(entry.get().getStore());
var view = new PrettyImageComp(new SimpleStringProperty(graphic), 45, 45);

View file

@ -43,7 +43,7 @@ public class OpenFileSystemComp extends SimpleComp {
private Region createContent() {
var overview = new Button(null, new FontIcon("mdi2m-monitor"));
overview.setOnAction(e -> model.cd(null));
overview.setOnAction(e -> model.cdSync(null));
overview.disableProperty().bind(model.getInOverview());
overview.setAccessibleText("System overview");

View file

@ -107,11 +107,11 @@ public final class OpenFileSystemModel {
return new FileSystem.FileEntry(fileSystem, currentPath.get(), null, false, false, 0, null, FileKind.DIRECTORY);
}
public void cd(String path) {
cdOrRetry(path, false).ifPresent(s -> cdOrRetry(s, false));
public void cdSync(String path) {
cdSyncOrRetry(path, false).ifPresent(s -> cdSyncOrRetry(s, false));
}
public Optional<String> cdOrRetry(String path, boolean allowCommands) {
public Optional<String> cdSyncOrRetry(String path, boolean allowCommands) {
if (Objects.equals(path, currentPath.get())) {
return Optional.empty();
}
@ -179,16 +179,12 @@ public final class OpenFileSystemModel {
try {
FileSystemHelper.validateDirectoryPath(this, resolvedPath);
cdSyncWithoutCheck(path);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
return Optional.ofNullable(currentPath.get());
}
ThreadHelper.runFailableAsync(() -> {
try (var ignored = new BusyProperty(busy)) {
cdSyncWithoutCheck(path);
}
});
return Optional.empty();
}

View file

@ -11,8 +11,10 @@ import javafx.css.Size;
import javafx.css.SizeUnits;
import javafx.scene.Node;
import javafx.scene.control.Button;
import lombok.Getter;
import org.kordamp.ikonli.javafx.FontIcon;
@Getter
public class ButtonComp extends Comp<CompStructure<Button>> {
private final ObservableValue<String> name;
@ -31,10 +33,6 @@ public class ButtonComp extends Comp<CompStructure<Button>> {
this.listener = listener;
}
public ObservableValue<String> getName() {
return name;
}
public Node getGraphic() {
return graphic.get();
}
@ -43,10 +41,6 @@ public class ButtonComp extends Comp<CompStructure<Button>> {
return graphic;
}
public Runnable getListener() {
return listener;
}
@Override
public CompStructure<Button> createBase() {
var button = new Button(null);

View file

@ -11,6 +11,7 @@ import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
@ -55,8 +56,9 @@ public class MarkdownComp extends Comp<CompStructure<StackPane>> {
@SneakyThrows
private WebView createWebView() {
var wv = new WebView();
wv.setPageFill(Color.valueOf("#EEE"));
var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, "web/github-markdown.css")
wv.setPageFill(Color.TRANSPARENT);
var theme = AppPrefs.get() != null && AppPrefs.get().theme.getValue().getTheme().isDarkMode() ? "web/github-markdown-dark.css" : "web/github-markdown-light.css";
var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, theme)
.orElseThrow();
wv.getEngine().setUserStyleSheetLocation(url.toString());

View file

@ -0,0 +1,59 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.storage.store.StoreEntryWrapper;
import io.xpipe.app.core.AppResources;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageComp;
import io.xpipe.app.fxcomps.impl.StackComp;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.impl.FileNames;
import javafx.beans.binding.Bindings;
import javafx.scene.layout.Region;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class OsLogoComp extends SimpleComp {
private final StoreEntryWrapper wrapper;
public OsLogoComp(StoreEntryWrapper wrapper) {
this.wrapper = wrapper;
}
@Override
protected Region createSimple() {
var img = Bindings.createObjectBinding(
() -> {
return wrapper.getState().getValue() == DataStoreEntry.State.COMPLETE_AND_VALID
? getImage(wrapper.getInformation().get()) : null;
},
wrapper.getState(), wrapper.getInformation());
return new StackComp(List.of(new SystemStateComp(wrapper).hide(img.isNotNull()), new PrettyImageComp(img, 24, 24))).createRegion();
}
private static final Map<String, String> ICONS = new HashMap<>();
private static final String LINUX_DEFAULT = "linux.svg";
private String getImage(String name) {
if (name == null) {
return null;
}
if (ICONS.isEmpty()) {
AppResources.withResource(AppResources.XPIPE_MODULE, "img/os", ModuleLayer.boot(), file -> {
try (var list = Files.list(file)) {
list.filter(path -> !path.toString().endsWith(LINUX_DEFAULT)).map(path -> FileNames.getFileName(path.toString())).forEach(path -> {
var base = FileNames.getBaseName(path).replace("-dark", "") + ".svg";
ICONS.put(FileNames.getBaseName(base).split("-")[0], "os/" + base);
});
}
});
}
var found = ICONS.entrySet().stream().filter(e->name.toLowerCase().contains(e.getKey())).findAny().map(e->e.getValue()).orElse("os/" + LINUX_DEFAULT);
return found;
}
}

View file

@ -1,7 +1,6 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
@ -39,7 +38,6 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
var selected = PseudoClass.getPseudoClass("selected");
entries.forEach(e -> {
var fi = new FontIcon(e.icon());
var b = new IconButtonComp(e.icon(), () -> value.setValue(e)).apply(new FancyTooltipAugment<>(e.name()));
b.apply(struc -> {
AppFont.setSize(struc.get(), 2);
@ -55,7 +53,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
{
var fi = new FontIcon("mdi2u-update");
var b = new BigIconButton(AppI18n.observable("update"), fi, () -> UpdateAvailableAlert.showIfNeeded());
var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableAlert.showIfNeeded());
b.hide(PlatformThread.sync(Bindings.createBooleanBinding(
() -> {
return XPipeDistributionType.get()

View file

@ -1,10 +1,11 @@
package io.xpipe.app.comp.base;
import atlantafx.base.theme.Styles;
import io.xpipe.app.comp.storage.store.StoreEntryWrapper;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.storage.DataStoreEntry;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
@ -15,8 +16,7 @@ import org.kordamp.ikonli.javafx.StackedFontIcon;
public class SystemStateComp extends SimpleComp {
public SystemStateComp(ObservableValue<String> name, ObservableValue<State> state) {
this.name = name;
public SystemStateComp(ObservableValue<State> state) {
this.state = state;
}
@ -26,9 +26,21 @@ public class SystemStateComp extends SimpleComp {
OTHER
}
private final ObservableValue<String> name;
private final ObservableValue<State> state;
public SystemStateComp(StoreEntryWrapper w) {
var state = Bindings.createObjectBinding(
() -> {
return w.getState().getValue() == DataStoreEntry.State.COMPLETE_BUT_INVALID
? SystemStateComp.State.FAILURE
: w.getState().getValue() == DataStoreEntry.State.COMPLETE_AND_VALID
? SystemStateComp.State.SUCCESS
: SystemStateComp.State.OTHER;
},
w.getState());
this.state = state;
}
@Override
protected Region createSimple() {
var icon = PlatformThread.sync(Bindings.createStringBinding(
@ -69,7 +81,6 @@ public class SystemStateComp extends SimpleComp {
pane.getStylesheets().add(val == State.SUCCESS ? success : val == State.FAILURE ? failure: other);
});
new FancyTooltipAugment<>(PlatformThread.sync(name)).augment(pane);
return pane;
}
}

View file

@ -20,9 +20,9 @@ public class StandardStoreEntryComp extends StoreEntryComp {
grid.setHgap(10);
grid.setVgap(0);
var storeIcon = createIcon(60, 45);
var storeIcon = createIcon(45, 35);
grid.add(storeIcon, 0, 0, 1, 2);
grid.getColumnConstraints().add(new ColumnConstraints(60));
grid.getColumnConstraints().add(new ColumnConstraints(50));
grid.add(name, 1, 0);
grid.add(createSummary(), 1, 1);

View file

@ -6,7 +6,6 @@ import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.PlatformState;
import io.xpipe.core.process.OsType;
import javafx.application.Application;
import javafx.application.Platform;
@ -29,7 +28,6 @@ public class App extends Application {
public void start(Stage primaryStage) {
TrackEvent.info("Application launched");
APP = this;
PlatformState.setCurrent(PlatformState.RUNNING);
stage = primaryStage;
// Set dock icon explicitly on mac
@ -47,7 +45,6 @@ public class App extends Application {
}
AppWindowHelper.addIcons(stage);
Platform.setImplicitExit(false);
}
public void close() {

View file

@ -0,0 +1,61 @@
package io.xpipe.app.core;
import io.xpipe.app.comp.base.MarkdownComp;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.util.PlatformState;
import io.xpipe.app.util.WindowsRegistry;
import io.xpipe.core.process.OsType;
import javafx.geometry.Insets;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import java.nio.file.Files;
import java.util.Optional;
public class AppAntivirusAlert {
public static Optional<String> detect() {
var bitdefender = WindowsRegistry.readString(WindowsRegistry.HKEY_LOCAL_MACHINE,"SOFTWARE\\Bitdefender", "InstallDir");
if (bitdefender.isPresent()) {
return Optional.of("Bitdefender");
}
return Optional.empty();
}
public static void showIfNeeded() throws Throwable {
// Only show this on first launch on windows
if (OsType.getLocal() != OsType.WINDOWS || !AppState.get().isInitialLaunch()) {
return;
}
var found = detect();
if (found.isEmpty()) {
return;
}
PlatformState.initPlatformOrThrow();
AppStyle.init();
AppImages.init();
var a = AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("antivirusNoticeTitle"));
alert.setAlertType(Alert.AlertType.NONE);
AppResources.withResource(
AppResources.XPIPE_MODULE,
"misc/antivirus.md",
AppExtensionManager.getInstance().getExtendedLayer(),
file -> {
var markdown = new MarkdownComp(Files.readString(file), s -> s.formatted(found.get(), found.get(), AppProperties.get().getVersion(), AppProperties.get().getVersion(), found.get())).prefWidth(550).prefHeight(600).createRegion();
alert.getDialogPane().setContent(markdown);
alert.getDialogPane().setPadding(new Insets(15));
});
alert.getButtonTypes().add(new ButtonType(AppI18n.get("gotIt"), ButtonBar.ButtonData.OK_DONE));
});
a.filter(b -> b.getButtonData().isDefaultButton())
.ifPresentOrElse(buttonType -> {}, () -> OperationMode.halt(1));
}
}

View file

@ -39,6 +39,11 @@ public class AppFileWatcher {
}
public void startWatchersInDirectories(List<Path> dirs, BiConsumer<Path, WatchEvent.Kind<Path>> listener) {
// Check in case initialization failed
if (watchService == null) {
return;
}
dirs.forEach(d -> watchedDirectories.add(new WatchedDirectory(d, listener)));
}
@ -78,6 +83,11 @@ public class AppFileWatcher {
}
private void stopWatcher() {
// Check in case initialization failed
if (watchService == null) {
return;
}
active = false;
watchedDirectories.clear();

View file

@ -24,6 +24,10 @@ public class AppImages {
private static final Map<String, String> svgImages = new HashMap<>();
public static void init() {
if (images.size() > 0 || svgImages.size() > 0) {
return;
}
TrackEvent.info("Loading images ...");
for (var module : AppExtensionManager.getInstance().getContentModules()) {
loadDirectory(module.getName(), "img");

View file

@ -39,6 +39,10 @@ public class AppLayoutModel {
selected.setValue(entries.get(0));
}
public void selectSettings() {
selected.setValue(entries.get(2));
}
public void selectConnections() {
selected.setValue(entries.get(1));
}

View file

@ -3,6 +3,7 @@ package io.xpipe.app.core;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.util.ModuleHelper;
import lombok.Getter;
import lombok.Value;
import java.nio.file.InvalidPathException;
@ -27,6 +28,7 @@ public class AppProperties {
String arch;
boolean image;
boolean staging;
@Getter
Path dataDir;
boolean showcase;
@ -43,10 +45,10 @@ public class AppProperties {
.orElse(UUID.randomUUID());
sentryUrl = System.getProperty("io.xpipe.app.sentryUrl");
arch = System.getProperty("io.xpipe.app.arch");
dataDir = parseDataDir();
staging = Optional.ofNullable(System.getProperty("io.xpipe.app.staging"))
.map(Boolean::parseBoolean)
.orElse(false);
dataDir = parseDataDir();
showcase = Optional.ofNullable(System.getProperty("io.xpipe.app.showcase"))
.map(Boolean::parseBoolean)
.orElse(false);
@ -93,7 +95,7 @@ public class AppProperties {
return INSTANCE;
}
private static Path parseDataDir() {
private Path parseDataDir() {
if (System.getProperty(DATA_DIR_PROP) != null) {
try {
return Path.of(System.getProperty(DATA_DIR_PROP));
@ -101,11 +103,7 @@ public class AppProperties {
}
}
return Path.of(System.getProperty("user.home"), ".xpipe");
}
public Path getDataDir() {
return dataDir;
return Path.of(System.getProperty("user.home"), isStaging() ? ".xpipe_stage" : ".xpipe");
}
public String getVersion() {

View file

@ -13,11 +13,9 @@ import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.css.PseudoClass;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Window;
import javafx.util.Duration;
import lombok.AllArgsConstructor;
@ -25,13 +23,6 @@ import lombok.Getter;
public class AppTheme {
public record AccentColor(Color primaryColor, PseudoClass pseudoClass) {
public static AccentColor xpipeBlue() {
return new AccentColor(Color.web("#11B4B4"), PseudoClass.getPseudoClass("accent-primer-purple"));
}
}
public static void init() {
if (AppPrefs.get() == null) {
return;

View file

@ -25,19 +25,17 @@ public class BaseMode extends OperationMode {
}
@Override
public void onSwitchTo() {}
@Override
public void onSwitchFrom() {}
@Override
public void initialSetup() throws Exception {
public void onSwitchTo() throws Throwable {
TrackEvent.info("mode", "Initializing base mode components ...");
AppExtensionManager.init(true);
JacksonMapper.initModularized(AppExtensionManager.getInstance().getExtendedLayer());
JacksonMapper.configure(objectMapper -> {
objectMapper.registerSubtypes(LockedSecretValue.class, DefaultSecretValue.class);
});
// Load translations before storage initialization to localize store error messages
// Also loaded before antivirus alert to localize that
AppI18n.init();
AppAntivirusAlert.showIfNeeded();
LocalStore.init();
AppPrefs.init();
AppCharsets.init();
@ -49,6 +47,9 @@ public class BaseMode extends OperationMode {
TrackEvent.info("mode", "Finished base components initialization");
}
@Override
public void onSwitchFrom() {}
@Override
public void finalTeardown() {
TrackEvent.info("mode", "Background mode shutdown started");

View file

@ -18,11 +18,8 @@ public class GuiMode extends PlatformMode {
}
@Override
public void onSwitchTo() {
if (PlatformState.getCurrent() == PlatformState.NOT_INITIALIZED) {
super.platformSetup();
}
public void onSwitchTo() throws Throwable {
super.platformSetup();
CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(() -> {
try {

View file

@ -215,20 +215,20 @@ public abstract class OperationMode {
OperationMode.halt(hasError ? 1 : 0);
}
public static synchronized void reload() {
ThreadHelper.create("reloader", false, () -> {
try {
switchTo(BACKGROUND);
CURRENT.finalTeardown();
CURRENT.initialSetup();
switchTo(GUI);
} catch (Throwable t) {
ErrorEvent.fromThrowable(t).build().handle();
OperationMode.halt(1);
}
})
.start();
}
// public static synchronized void reload() {
// ThreadHelper.create("reloader", false, () -> {
// try {
// switchTo(BACKGROUND);
// CURRENT.finalTeardown();
// CURRENT.onSwitchTo();
// switchTo(GUI);
// } catch (Throwable t) {
// ErrorEvent.fromThrowable(t).build().handle();
// OperationMode.halt(1);
// }
// })
// .start();
// }
private static synchronized void set(OperationMode newMode) {
if (CURRENT == null && newMode == null) {
@ -240,17 +240,17 @@ public abstract class OperationMode {
}
try {
if (CURRENT == null) {
CURRENT = newMode;
newMode.initialSetup();
} else if (newMode == null) {
if (newMode == null) {
shutdown(false, false);
} else {
var cur = CURRENT;
cur.onSwitchFrom();
CURRENT = newMode;
newMode.onSwitchTo();
return;
}
if (CURRENT != null) {
CURRENT.onSwitchFrom();
}
newMode.onSwitchTo();
CURRENT = newMode;
} catch (Throwable ex) {
ErrorEvent.fromThrowable(ex).terminal(true).build().handle();
}
@ -268,12 +268,10 @@ public abstract class OperationMode {
public abstract String getId();
public abstract void onSwitchTo();
public abstract void onSwitchTo() throws Throwable;
public abstract void onSwitchFrom();
public abstract void initialSetup() throws Throwable;
public abstract void finalTeardown() throws Throwable;
public abstract ErrorHandler getErrorHandler();

View file

@ -10,54 +10,28 @@ import io.xpipe.app.util.ThreadHelper;
import javafx.application.Application;
import javafx.application.Platform;
import java.awt.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public abstract class PlatformMode extends OperationMode {
private static boolean stateInitialized;
public static boolean HAS_GRAPHICS;
public static boolean PLATFORM_LOADED;
@Override
public boolean isSupported() {
PlatformState.initPlatform();
return PlatformState.getCurrent() == PlatformState.RUNNING;
}
private static void initState() {
if (stateInitialized) {
protected void platformSetup() throws Throwable {
if (App.getApp() != null) {
return;
}
try {
GraphicsDevice[] screenDevices =
GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();
HAS_GRAPHICS = screenDevices != null && screenDevices.length > 0;
} catch (HeadlessException e) {
TrackEvent.warn(e.getMessage());
HAS_GRAPHICS = false;
}
try {
Platform.startup(() -> {});
PLATFORM_LOADED = true;
} catch (Throwable t) {
TrackEvent.warn(t.getMessage());
PLATFORM_LOADED = false;
}
stateInitialized = true;
}
@Override
public boolean isSupported() {
initState();
return HAS_GRAPHICS && PLATFORM_LOADED;
}
protected void platformSetup() {
if (App.getApp() != null) {
throw new AssertionError();
}
TrackEvent.info("mode", "Platform mode initial setup");
AppI18n.init();
var r = PlatformState.initPlatform();
if (r.isPresent()) {
throw r.get();
}
AppFont.loadFonts();
AppTheme.init();
AppStyle.init();
@ -107,18 +81,11 @@ public abstract class PlatformMode extends OperationMode {
}
}
@Override
public void initialSetup() throws Throwable {
BACKGROUND.initialSetup();
onSwitchTo();
}
@Override
public void finalTeardown() throws Throwable {
TrackEvent.info("mode", "Shutting down platform components");
onSwitchFrom();
Platform.exit();
PlatformState.setCurrent(PlatformState.EXITED);
PlatformState.teardown();
TrackEvent.info("mode", "Platform shutdown finished");
BACKGROUND.finalTeardown();
}

View file

@ -3,7 +3,6 @@ package io.xpipe.app.core.mode;
import com.dustinredmond.fxtrayicon.FXTrayIcon;
import io.xpipe.app.core.AppTray;
import io.xpipe.app.issue.*;
import io.xpipe.app.util.PlatformState;
public class TrayMode extends PlatformMode {
@ -18,10 +17,8 @@ public class TrayMode extends PlatformMode {
}
@Override
public void onSwitchTo() {
if (PlatformState.getCurrent() == PlatformState.NOT_INITIALIZED) {
super.platformSetup();
}
public void onSwitchTo() throws Throwable {
super.platformSetup();
if (AppTray.get() == null) {
TrackEvent.info("mode", "Initializing tray");

View file

@ -12,7 +12,7 @@ public class LaunchExchangeImpl extends LaunchExchange
@Override
public Response handleRequest(BeaconHandler handler, Request msg) throws Exception {
var store = getStoreEntryByName(msg.getName(), false);
var store = getStoreEntryById(msg.getId(), false);
if (store.getStore() instanceof LaunchableStore s) {
var command = s.prepareLaunchCommand(store.getName());
var split = CommandLine.parse(command);

View file

@ -12,6 +12,7 @@ import io.xpipe.beacon.exchange.MessageExchange;
import io.xpipe.core.dialog.Dialog;
import io.xpipe.core.impl.NamedStore;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.source.DataStoreId;
import io.xpipe.core.store.DataStore;
import lombok.NonNull;
@ -63,6 +64,18 @@ public interface MessageExchangeImpl<RQ extends RequestMessage, RS extends Respo
return store.get();
}
default DataStoreEntry getStoreEntryById(@NonNull DataStoreId id, boolean acceptDisabled) throws ClientException {
var store = DataStorage.get().getStoreEntryIfPresent(id);
if (store.isEmpty()) {
throw new ClientException("No store with id " + id + " was found");
}
if (store.get().isDisabled() && !acceptDisabled) {
throw new ClientException(
String.format("Store %s is disabled", store.get().getName()));
}
return store.get();
}
String getId();
RS handleRequest(BeaconHandler handler, RQ msg) throws Exception;

View file

@ -8,8 +8,6 @@ import io.xpipe.beacon.exchange.ReadExchange;
import io.xpipe.core.dialog.Dialog;
import io.xpipe.core.dialog.QueryConverter;
import java.util.UUID;
public class ReadExchangeImpl extends ReadExchange
implements MessageExchangeImpl<ReadExchange.Request, ReadExchange.Response> {
@ -46,11 +44,6 @@ public class ReadExchangeImpl extends ReadExchange
return toCompleteConfig(defaultDesc, provider, msg.isConfigureAll());
});
var noTarget = msg.getTarget() == null;
var colName = noTarget ? null : msg.getTarget().getCollectionName();
var entryName =
noTarget ? UUID.randomUUID().toString() : msg.getTarget().getEntryName();
return Response.builder().build();
}
}

View file

@ -0,0 +1,30 @@
package io.xpipe.app.exchange.cli;
import io.xpipe.app.exchange.MessageExchangeImpl;
import io.xpipe.beacon.BeaconHandler;
import io.xpipe.beacon.ClientException;
import io.xpipe.beacon.exchange.DrainExchange;
import io.xpipe.core.store.ShellStore;
public class DrainExchangeImpl extends DrainExchange
implements MessageExchangeImpl<DrainExchange.Request, DrainExchange.Response> {
@Override
public Response handleRequest(BeaconHandler handler, Request msg) throws Exception {
var ds = getStoreEntryById(msg.getSource(), false);
if (!(ds.getStore() instanceof ShellStore)) {
throw new ClientException("Can't open file system for connection");
}
handler.postResponse(() -> {
ShellStore store = ds.getStore().asNeeded();
try (var fs = store.createFileSystem();
var output = handler.sendBody();
var inputStream = fs.openInput(msg.getPath())) {
inputStream.transferTo(output);
}
});
return Response.builder().build();
}
}

View file

@ -15,12 +15,13 @@ public class ListStoresExchangeImpl extends ListStoresExchange
public Response handleRequest(BeaconHandler handler, Request msg) {
DataStorage s = DataStorage.get();
var e = s.getStoreEntries().stream()
.filter(entry -> !entry.isDisabled() && entry.getProvider().canManuallyCreate())
.sorted(Comparator.comparing(dataStoreEntry -> dataStoreEntry.getLastUsed()))
.filter(entry -> !entry.isDisabled())
.map(col -> StoreListEntry.builder()
.name(col.getName())
.id(DataStorage.get().getId(col))
.type(col.getProvider().getId())
.information(col.getInformation())
.build())
.sorted(Comparator.comparing(en -> en.getId().toString()))
.toList();
return Response.builder().entries(e).build();
}

View file

@ -1,42 +1,14 @@
package io.xpipe.app.exchange.cli;
import io.xpipe.app.exchange.MessageExchangeImpl;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.beacon.BeaconHandler;
import io.xpipe.beacon.ServerException;
import io.xpipe.beacon.exchange.cli.ReadDrainExchange;
import io.xpipe.core.impl.SinkDrainStore;
import io.xpipe.core.store.StreamDataStore;
import java.io.InputStream;
import java.util.Optional;
public class ReadDrainExchangeImpl extends ReadDrainExchange
implements MessageExchangeImpl<ReadDrainExchange.Request, ReadDrainExchange.Response> {
@Override
public Response handleRequest(BeaconHandler handler, Request msg) throws Exception {
var ds = DataStorage.get().getStoreEntryIfPresent(msg.getName());
if (ds.isEmpty()) {
ds = Optional.of(DataStorage.get().addStoreEntry(msg.getName(), msg.getStore()));
}
if (!(ds.get().getStore() instanceof SinkDrainStore)) {
throw new ServerException("Data store is not a drain");
}
DataStoreEntry finalDs = ds.get();
handler.postResponse(() -> {
ThreadHelper.runFailableAsync(() -> {
StreamDataStore store = finalDs.getStore().asNeeded();
try (var output = handler.sendBody();
InputStream inputStream = store.openInput()) {
inputStream.transferTo(output);
}
});
});
return ReadDrainExchange.Response.builder().build();
}
}

View file

@ -0,0 +1,29 @@
package io.xpipe.app.exchange.cli;
import io.xpipe.app.exchange.MessageExchangeImpl;
import io.xpipe.beacon.BeaconHandler;
import io.xpipe.beacon.ClientException;
import io.xpipe.beacon.exchange.SinkExchange;
import io.xpipe.core.store.ShellStore;
public class SinkExchangeImpl extends SinkExchange
implements MessageExchangeImpl<SinkExchange.Request, SinkExchange.Response> {
@Override
public Response handleRequest(BeaconHandler handler, Request msg) throws Exception {
var ds = getStoreEntryById(msg.getSource(), false);
if (!(ds.getStore() instanceof ShellStore)) {
throw new ClientException("Can't open file system for connection");
}
ShellStore store = ds.getStore().asNeeded();
try (var fs = store.createFileSystem();
var inputStream = handler.receiveBody();
var output = fs.openOutput(msg.getPath())) {
inputStream.transferTo(output);
}
return Response.builder().build();
}
}

View file

@ -2,7 +2,7 @@ package io.xpipe.app.ext;
import io.xpipe.app.util.Validator;
import io.xpipe.core.source.DataSource;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataStoreId;
import io.xpipe.core.util.ModuleLayerLoader;
import javafx.beans.value.ObservableValue;
import javafx.scene.layout.Region;
@ -47,11 +47,11 @@ public interface DataSourceTarget {
return ALL;
}
default InstructionsDisplay createRetrievalInstructions(DataSource<?> source, ObservableValue<DataSourceId> id) {
default InstructionsDisplay createRetrievalInstructions(DataSource<?> source, ObservableValue<DataStoreId> id) {
return null;
}
default InstructionsDisplay createUpdateInstructions(DataSource<?> source, ObservableValue<DataSourceId> id) {
default InstructionsDisplay createUpdateInstructions(DataSource<?> source, ObservableValue<DataStoreId> id) {
return null;
}

View file

@ -7,6 +7,7 @@ import io.xpipe.app.comp.storage.store.StoreSectionComp;
import io.xpipe.app.comp.storage.store.StoreEntryWrapper;
import io.xpipe.app.comp.storage.store.StoreSection;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppImages;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.dialog.Dialog;
@ -65,12 +66,7 @@ public interface DataStoreProvider {
: SystemStateComp.State.OTHER;
},
w.getState());
var name = Bindings.createStringBinding(
() -> {
return w.getState().getValue() == DataStoreEntry.State.COMPLETE_BUT_INVALID ? "stop" : "start";
},
w.getState());
return new SystemStateComp(name, state);
return new SystemStateComp(state);
}
default Comp<?> createInsightsComp(ObservableValue<DataStore> store) {
@ -159,7 +155,13 @@ public interface DataStoreProvider {
}
default String getDisplayIconFileName(DataStore store) {
return getModuleName() + ":" + getId() + "_icon.png";
var png = getModuleName() + ":" + getId() + "_icon.png";
if (AppImages.hasNormalImage(png)) {
return png;
}
var svg = getModuleName() + ":" + getId() + "_icon.svg";
return svg;
}
default Dialog dialogForStore(DataStore store) {

View file

@ -17,6 +17,7 @@ public class DataStoreProviders {
if (ALL == null) {
ALL = ServiceLoader.load(layer, DataStoreProvider.class).stream()
.map(ServiceLoader.Provider::get)
.sorted(Comparator.comparing(DataStoreProvider::getId))
.collect(Collectors.toList());
ALL.removeIf(p -> {
try {
@ -35,10 +36,6 @@ public class DataStoreProviders {
}
public static void postInit(ModuleLayer layer) {
ALL = ServiceLoader.load(layer, DataStoreProvider.class).stream()
.map(ServiceLoader.Provider::get)
.sorted(Comparator.comparing(DataStoreProvider::getId))
.collect(Collectors.toList());
ALL.forEach(p -> {
try {
p.postInit();

View file

@ -33,6 +33,7 @@ public class ChoicePaneComp extends Comp<CompStructure<VBox>> {
public CompStructure<VBox> createBase() {
var list = FXCollections.observableArrayList(entries);
var cb = new ComboBox<>(list);
cb.getSelectionModel().select(selected.getValue());
cb.setConverter(new StringConverter<>() {
@Override
public String toString(Entry object) {
@ -52,7 +53,7 @@ public class ChoicePaneComp extends Comp<CompStructure<VBox>> {
var vbox = new VBox(transformer.apply(cb));
vbox.setFillWidth(true);
cb.prefWidthProperty().bind(vbox.widthProperty());
cb.valueProperty().addListener((c, o, n) -> {
SimpleChangeListener.apply(cb.valueProperty(), n-> {
if (n == null) {
vbox.getChildren().remove(1);
} else {

View file

@ -28,7 +28,7 @@ public class FileStoreChoiceComp extends SimpleComp {
public FileStoreChoiceComp(boolean hideFileSystem, Property<FileSystemStore> fileSystem, Property<String> filePath) {
this.hideFileSystem = hideFileSystem;
this.fileSystem = fileSystem;
this.fileSystem = fileSystem != null ? fileSystem : new SimpleObjectProperty<>();
this.filePath = filePath;
}

View file

@ -152,18 +152,15 @@ public class OptionsComp extends Comp<CompStructure<Pane>> {
if (entries.stream().anyMatch(entry -> entry.name() != null && entry.description() == null)) {
var nameWidthBinding = Bindings.createDoubleBinding(
() -> {
if (nameRegions.stream().anyMatch(r -> r.getWidth() == 0)) {
return Region.USE_COMPUTED_SIZE;
}
var m = nameRegions.stream()
.map(Region::getWidth)
.filter(aDouble -> aDouble > 0.0)
.max(Double::compareTo)
.orElse(0.0);
.orElse(Region.USE_COMPUTED_SIZE);
return m;
},
nameRegions.stream().map(Region::widthProperty).toList().toArray(new Observable[0]));
nameRegions.forEach(r -> r.prefWidthProperty().bind(nameWidthBinding));
nameRegions.forEach(r -> r.minWidthProperty().bind(nameWidthBinding));
}
return new SimpleCompStructure<>(pane);

View file

@ -4,6 +4,8 @@ import io.xpipe.app.core.AppImages;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.impl.FileNames;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
@ -13,6 +15,8 @@ import javafx.scene.image.ImageView;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import java.util.function.Consumer;
public class PrettyImageComp extends SimpleComp {
private final ObservableValue<String> value;
@ -57,8 +61,17 @@ public class PrettyImageComp extends SimpleComp {
var svgImageContent = new SimpleStringProperty();
var storeIcon = SvgView.create(svgImageContent);
SimpleChangeListener.apply(image, newValue -> {
if (newValue == null) {
svgImageContent.set(null);
return;
}
if (AppImages.hasSvgImage(newValue)) {
svgImageContent.set(AppImages.svgImage(newValue));
} else if (AppImages.hasSvgImage(newValue.replace("-dark", ""))) {
svgImageContent.set(AppImages.svgImage(newValue.replace("-dark", "")));
} else {
svgImageContent.set(null);
}
});
var ar = Bindings.createDoubleBinding(
@ -86,11 +99,17 @@ public class PrettyImageComp extends SimpleComp {
.imageProperty()
.bind(Bindings.createObjectBinding(
() -> {
if (!AppImages.hasNormalImage(image.getValue())) {
if (image.get() == null) {
return null;
}
return AppImages.image(image.getValue());
if (AppImages.hasNormalImage(image.getValue())) {
return AppImages.image(image.getValue());
} else if (AppImages.hasNormalImage(image.getValue().replace("-dark", ""))) {
return AppImages.image(image.getValue().replace("-dark", ""));
} else {
return null;
}
},
image));
var ar = Bindings.createDoubleBinding(
@ -110,8 +129,9 @@ public class PrettyImageComp extends SimpleComp {
stack.getChildren().add(storeIcon);
}
SimpleChangeListener.apply(PlatformThread.sync(value), val -> {
image.set(val);
Consumer<String> update = val -> {
var fixed = val != null ? FileNames.getBaseName(val) + (AppPrefs.get().theme.get().getTheme().isDarkMode() ? "-dark" : "") + "." + FileNames.getExtension(val) : null;
image.set(fixed);
aspectRatioProperty.unbind();
if (val == null) {
@ -126,6 +146,11 @@ public class PrettyImageComp extends SimpleComp {
stack.getChildren().get(0).setOpacity(0.0);
stack.getChildren().get(1).setOpacity(1.0);
}
};
SimpleChangeListener.apply(PlatformThread.sync(value), val -> update.accept(val));
AppPrefs.get().theme.addListener((observable, oldValue, newValue) -> {
update.accept(value.getValue());
});
stack.setFocusTraversable(false);

View file

@ -10,6 +10,8 @@ import javafx.beans.property.Property;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import java.util.Objects;
public class SecretFieldComp extends Comp<CompStructure<TextField>> {
private final Property<SecretValue> value;
@ -32,6 +34,11 @@ public class SecretFieldComp extends Comp<CompStructure<TextField>> {
});
value.addListener((c, o, n) -> {
PlatformThread.runLaterIfNeeded(() -> {
// Check if control value is the same. Then don't set it as that might cause bugs
if ((n == null && text.getText().isEmpty()) || Objects.equals(text.getText(), n != null ? n.getSecretValue() : null)) {
return;
}
text.setText(n != null ? n.getSecretValue() : null);
});
});

View file

@ -41,6 +41,12 @@ public class TextAreaComp extends SimpleComp {
lastAppliedValue.addListener((c, o, n) -> {
currentValue.setValue(n);
PlatformThread.runLaterIfNeeded(() -> {
// Check if control value is the same. Then don't set it as that might cause bugs
if (Objects.equals(text.getText(), n)
|| (n == null && text.getText().isEmpty())) {
return;
}
text.setText(n);
});
});

View file

@ -41,6 +41,7 @@ public class SentryErrorHandler implements ErrorHandler {
options.setTag("os", System.getProperty("os.name"));
options.setTag("osVersion", System.getProperty("os.version"));
options.setTag("arch", System.getProperty("os.arch"));
options.setTag("updatesEnabled", AppPrefs.get() != null ? AppPrefs.get().automaticallyUpdate().getValue().toString() : "unknown");
options.setDist(XPipeDistributionType.get().getId());
if (AppProperties.get().isStaging()) {
options.setTag("staging", "true");

View file

@ -6,6 +6,7 @@ import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.LogErrorHandler;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.beacon.BeaconServer;
import io.xpipe.beacon.exchange.FocusExchange;
@ -112,12 +113,14 @@ public class LauncherCommand implements Callable<Integer> {
return XPipeDaemonMode.get(opModeName);
}
return XPipeDaemonMode.GUI;
return AppPrefs.get() != null ? AppPrefs.get().startupBehaviour().getValue().getMode() : XPipeDaemonMode.GUI;
}
@Override
public Integer call() {
checkStart();
// Initialize base mode first to have access to the preferences to determine effective mode
OperationMode.switchTo(OperationMode.BACKGROUND);
OperationMode.switchTo(OperationMode.map(getEffectiveMode()));
LauncherInput.handle(inputs);

View file

@ -13,10 +13,12 @@ import javafx.beans.property.ObjectProperty;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.control.ScrollPane;
import lombok.Getter;
import lombok.SneakyThrows;
import java.util.List;
@Getter
public class AppPreferencesFx {
private final PreferencesFxModel preferencesFxModel;

View file

@ -1,5 +1,6 @@
package io.xpipe.app.prefs;
import atlantafx.base.theme.Styles;
import com.dlsc.formsfx.model.structure.*;
import com.dlsc.preferencesfx.formsfx.view.controls.SimpleComboBoxControl;
import com.dlsc.preferencesfx.formsfx.view.controls.SimpleControl;
@ -10,15 +11,21 @@ import com.dlsc.preferencesfx.model.Setting;
import com.dlsc.preferencesfx.util.VisibilityProperty;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.AppTheme;
import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.ext.PrefsHandler;
import io.xpipe.app.ext.PrefsProvider;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.StackComp;
import io.xpipe.app.fxcomps.impl.TextFieldComp;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.LockChangeAlert;
import io.xpipe.app.util.LockedSecretValue;
import io.xpipe.app.util.*;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.process.CommandControl;
import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.util.ModuleHelper;
import io.xpipe.core.util.SecretValue;
import javafx.beans.binding.Bindings;
@ -26,10 +33,12 @@ import javafx.beans.property.*;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import lombok.SneakyThrows;
import org.kordamp.ikonli.javafx.FontIcon;
import java.nio.file.Path;
import java.util.*;
@ -65,7 +74,7 @@ public class AppPrefs {
private static final Path DEFAULT_STORAGE_DIR =
AppProperties.get().getDataDir().resolve("storage");
private static final boolean STORAGE_DIR_FIXED =
!AppProperties.get().getDataDir().equals(AppProperties.DEFAULT_DATA_DIR);
!AppProperties.get().getDataDir().equals(Path.of(System.getProperty("user.home"), AppProperties.get().isStaging() ? ".xpipe_stage" : ".xpipe"));
private static final String LOG_LEVEL_PROP = "io.xpipe.app.logLevel";
// Lets keep this at trace for now, at least for the alpha
private static final String DEFAULT_LOG_LEVEL = "debug";
@ -156,6 +165,10 @@ public class AppPrefs {
private final BooleanField preferTerminalTabsField =
BooleanField.ofBooleanType(preferTerminalTabs).render(() -> new CustomToggleControl());
// Password manager
// ================
private final StringProperty passwordManagerCommand = typed(new SimpleStringProperty(""), String.class);
// Start behaviour
// ===============
private final SimpleListProperty<StartupBehaviour> startupBehaviourList = new SimpleListProperty<>(
@ -163,9 +176,9 @@ public class AppPrefs {
private final ObjectProperty<StartupBehaviour> startupBehaviour =
typed(new SimpleObjectProperty<>(StartupBehaviour.GUI), StartupBehaviour.class);
private final SingleSelectionField<StartupBehaviour> startupBehaviourControl =
Field.ofSingleSelectionType(startupBehaviourList, startupBehaviour)
.render(() -> new TranslatableComboBoxControl<>());
private final SingleSelectionField<StartupBehaviour> startupBehaviourControl = Field.ofSingleSelectionType(
startupBehaviourList, startupBehaviour)
.render(() -> new TranslatableComboBoxControl<>());
// Close behaviour
// ===============
@ -209,10 +222,8 @@ public class AppPrefs {
// =======
private final ObjectProperty<Path> storageDirectory =
typed(new SimpleObjectProperty<>(DEFAULT_STORAGE_DIR), Path.class);
private final StringField storageDirectoryControl = PrefFields.ofPath(storageDirectory)
.validate(
CustomValidators.absolutePath(),
CustomValidators.directory());
private final StringField storageDirectoryControl =
PrefFields.ofPath(storageDirectory).validate(CustomValidators.absolutePath(), CustomValidators.directory());
// Log level
// =========
@ -240,30 +251,35 @@ public class AppPrefs {
typed(new SimpleBooleanProperty(false), Boolean.class);
private final BooleanField developerDisableUpdateVersionCheckField =
BooleanField.ofBooleanType(developerDisableUpdateVersionCheck).render(() -> new CustomToggleControl());
private final ObservableBooleanValue developerDisableUpdateVersionCheckEffective = bindDeveloperTrue(developerDisableUpdateVersionCheck);
private final ObservableBooleanValue developerDisableUpdateVersionCheckEffective =
bindDeveloperTrue(developerDisableUpdateVersionCheck);
private final BooleanProperty developerDisableGuiRestrictions =
typed(new SimpleBooleanProperty(false), Boolean.class);
private final BooleanField developerDisableGuiRestrictionsField =
BooleanField.ofBooleanType(developerDisableGuiRestrictions).render(() -> new CustomToggleControl());
private final ObservableBooleanValue developerDisableGuiRestrictionsEffective = bindDeveloperTrue(developerDisableGuiRestrictions);
private final ObservableBooleanValue developerDisableGuiRestrictionsEffective =
bindDeveloperTrue(developerDisableGuiRestrictions);
private final BooleanProperty developerShowHiddenProviders = typed(new SimpleBooleanProperty(false), Boolean.class);
private final BooleanField developerShowHiddenProvidersField =
BooleanField.ofBooleanType(developerShowHiddenProviders).render(() -> new CustomToggleControl());
private final ObservableBooleanValue developerShowHiddenProvidersEffective = bindDeveloperTrue(developerShowHiddenProviders);
private final ObservableBooleanValue developerShowHiddenProvidersEffective =
bindDeveloperTrue(developerShowHiddenProviders);
private final BooleanProperty developerShowHiddenEntries = typed(new SimpleBooleanProperty(false), Boolean.class);
private final BooleanField developerShowHiddenEntriesField =
BooleanField.ofBooleanType(developerShowHiddenEntries).render(() -> new CustomToggleControl());
private final ObservableBooleanValue developerShowHiddenEntriesEffective = bindDeveloperTrue(developerShowHiddenEntries);
private final ObservableBooleanValue developerShowHiddenEntriesEffective =
bindDeveloperTrue(developerShowHiddenEntries);
private final BooleanProperty developerDisableConnectorInstallationVersionCheck =
typed(new SimpleBooleanProperty(false), Boolean.class);
private final BooleanField developerDisableConnectorInstallationVersionCheckField = BooleanField.ofBooleanType(
developerDisableConnectorInstallationVersionCheck)
.render(() -> new CustomToggleControl());
private final ObservableBooleanValue developerDisableConnectorInstallationVersionCheckEffective = bindDeveloperTrue(developerDisableConnectorInstallationVersionCheck);
private final ObservableBooleanValue developerDisableConnectorInstallationVersionCheckEffective =
bindDeveloperTrue(developerDisableConnectorInstallationVersionCheck);
public ReadOnlyProperty<CloseBehaviour> closeBehaviour() {
return closeBehaviour;
@ -510,12 +526,102 @@ public class AppPrefs {
return null;
}
public void selectCategory(int index) {
AppLayoutModel.get().selectSettings();
preferencesFx
.getNavigationPresenter()
.setSelectedCategory(preferencesFx.getCategories().get(index));
}
public String passwordManagerString(String key) {
if (passwordManagerCommand.get() == null
|| passwordManagerCommand.get().isEmpty()
|| key == null
|| key.isEmpty()) {
return null;
}
return ApplicationHelper.replaceFileArgument(passwordManagerCommand.get(), "KEY", key);
}
@SneakyThrows
private AppPreferencesFx createPreferences() {
var ctr = Setting.class.getDeclaredConstructor(String.class, Element.class, Property.class);
ctr.setAccessible(true);
var terminalTest = ctr.newInstance(
null,
new LazyNodeElement<>(() -> new StackComp(
List.of(new ButtonComp(AppI18n.observable("test"), new FontIcon("mdi2p-play"), () -> {
ThreadHelper.runFailableAsync(() -> {
var term = AppPrefs.get().terminalType().getValue();
if (term != null) {
TerminalHelper.open(
"Test",
new LocalStore().control().command("echo Test"));
}
});
})))
.padding(new Insets(15, 0, 0, 0))
.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT))
.createRegion()),
null);
var editorTest = ctr.newInstance(
null,
new LazyNodeElement<>(() -> new StackComp(
List.of(new ButtonComp(AppI18n.observable("test"), new FontIcon("mdi2p-play"), () -> {
ThreadHelper.runFailableAsync(() -> {
var editor =
AppPrefs.get().externalEditor().getValue();
if (editor != null) {
FileOpener.openReadOnlyString("Test");
}
});
})))
.padding(new Insets(15, 0, 0, 0))
.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT))
.createRegion()),
null);
var about = ctr.newInstance(null, new LazyNodeElement<>(() -> new AboutComp().createRegion()), null);
var troubleshoot = ctr.newInstance(null, new LazyNodeElement<>(() -> new TroubleshootComp().createRegion()), null);
var troubleshoot =
ctr.newInstance(null, new LazyNodeElement<>(() -> new TroubleshootComp().createRegion()), null);
var testPasswordManagerValue = new SimpleStringProperty();
var testPasswordManager = ctr.newInstance(
"passwordManagerCommandTest",
new LazyNodeElement<>(() -> new HorizontalComp(List.of(
new TextFieldComp(testPasswordManagerValue)
.apply(struc -> struc.get().setPromptText("Test password key"))
.styleClass(Styles.LEFT_PILL)
.grow(false, true),
new ButtonComp(null, new FontIcon("mdi2p-play"), () -> {
var cmd = passwordManagerString(testPasswordManagerValue.get());
if (cmd == null) {
return;
}
try {
TerminalHelper.open(
"Password test",
new LocalStore()
.control()
.command(cmd
+ "\n" + ShellDialects.getPlatformDefault()
.getEchoCommand(
"Is this your password?", false))
.terminalExitMode(
CommandControl.TerminalExitMode
.KEEP_OPEN));
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
})
.styleClass(Styles.RIGHT_PILL)
.grow(false, true)))
.padding(new Insets(15, 0, 0, 0))
.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT))
.apply(struc -> struc.get().setFillHeight(true))
.createRegion()),
null);
var categories = new ArrayList<>(List.of(
Category.of("about", Group.of(about)),
@ -523,11 +629,7 @@ public class AppPrefs {
"system",
Group.of(
"appBehaviour",
Setting.of(
"startupBehaviour",
startupBehaviourControl,
startupBehaviour
),
Setting.of("startupBehaviour", startupBehaviourControl, startupBehaviour),
Setting.of("closeBehaviour", closeBehaviourControl, closeBehaviour)),
Group.of("security", Setting.of("workspaceLock", lockCryptControl, lockCrypt)),
Group.of(
@ -539,7 +641,9 @@ public class AppPrefs {
Setting.of("updateToPrereleases", checkForPrereleasesField, checkForPrereleases)),
group(
"advanced",
STORAGE_DIR_FIXED ? null : Setting.of("storageDirectory", storageDirectoryControl, storageDirectory),
STORAGE_DIR_FIXED
? null
: Setting.of("storageDirectory", storageDirectoryControl, storageDirectory),
Setting.of("logLevel", logLevelField, internalLogLevel),
Setting.of("developerMode", developerModeField, internalDeveloperMode))),
Category.of(
@ -551,10 +655,14 @@ public class AppPrefs {
Setting.of("tooltipDelay", tooltipDelayInternal, tooltipDelayMin, tooltipDelayMax),
Setting.of("language", languageControl, languageInternal)),
Group.of("windowOptions", Setting.of("saveWindowLocation", saveWindowLocationInternal))),
Category.of(
"passwordManager",
Group.of(Setting.of("passwordManagerCommand", passwordManagerCommand), testPasswordManager)),
Category.of(
"editor",
Group.of(
Setting.of("editorProgram", externalEditorControl, externalEditor),
editorTest,
Setting.of("customEditorCommand", customEditorCommandControl, customEditorCommand)
.applyVisibility(VisibilityProperty.of(
externalEditor.isEqualTo(ExternalEditorType.CUSTOM))),
@ -564,13 +672,15 @@ public class AppPrefs {
editorReloadTimeoutMin,
editorReloadTimeoutMax),
Setting.of("preferEditorTabs", preferEditorTabsField, preferEditorTabs))),
Category.of("terminal",
Group.of(
Setting.of("terminalProgram", terminalTypeControl, terminalType),
Setting.of("customTerminalCommand", customTerminalCommandControl, customTerminalCommand)
.applyVisibility(VisibilityProperty.of(
terminalType.isEqualTo(ExternalTerminalType.CUSTOM))),
Setting.of("preferTerminalTabs", preferTerminalTabsField, preferTerminalTabs))),
Category.of(
"terminal",
Group.of(
Setting.of("terminalProgram", terminalTypeControl, terminalType),
terminalTest,
Setting.of("customTerminalCommand", customTerminalCommandControl, customTerminalCommand)
.applyVisibility(VisibilityProperty.of(
terminalType.isEqualTo(ExternalTerminalType.CUSTOM))),
Setting.of("preferTerminalTabs", preferTerminalTabsField, preferTerminalTabs))),
Category.of(
"developer",
Setting.of(
@ -604,8 +714,9 @@ public class AppPrefs {
return AppPreferencesFx.of(cats);
}
private Group group(String name, Setting<?,?>... settings) {
return Group.of(name, Arrays.stream(settings).filter(setting -> setting != null).toArray(Setting[]::new));
private Group group(String name, Setting<?, ?>... settings) {
return Group.of(
name, Arrays.stream(settings).filter(setting -> setting != null).toArray(Setting[]::new));
}
private class PrefsHandlerImpl implements PrefsHandler {

View file

@ -2,6 +2,7 @@ package io.xpipe.app.prefs;
import com.dlsc.formsfx.model.validators.CustomValidator;
import com.dlsc.formsfx.model.validators.Validator;
import io.xpipe.app.core.AppI18n;
import java.nio.file.Files;
import java.nio.file.Path;
@ -18,7 +19,7 @@ public class CustomValidators {
return false;
}
},
"notAnAbsolutePath");
AppI18n.get("notAnAbsolutePath"));
}
public static Validator<String> directory() {
@ -27,6 +28,6 @@ public class CustomValidators {
var p = Path.of(s);
return Files.exists(p) && Files.isDirectory(p);
},
"notADirectory");
AppI18n.get("notADirectory"));
}
}

View file

@ -5,6 +5,7 @@ import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialects;
import java.nio.file.Files;
import java.nio.file.Path;
@ -52,7 +53,23 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue {
if (c.getExitCode() != 0 || path.isBlank()) {
return Optional.empty();
}
return Optional.of(Path.of(path));
// Check if returned paths are actually valid
var valid = path.lines().filter(s -> {
try {
return Files.exists(Path.of(s));
} catch (Exception ex) {
return false;
}
}).toList();
// Prefer app in proper applications directory
var app = valid.stream().filter(s -> s.startsWith("/Applications")).findFirst();
if (app.isPresent()) {
return app.map(Path::of);
}
return valid.stream().findFirst().map(Path::of);
}
} catch (Exception e) {
ErrorEvent.fromThrowable(e).omit().handle();
@ -90,13 +107,35 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue {
}
}
public abstract static class WindowsFullPathType extends ExternalApplicationType {
public abstract static class WindowsType extends ExternalApplicationType {
public WindowsFullPathType(String id) {
private final String executable;
public WindowsType(String id, String executable) {
super(id);
this.executable = executable;
}
protected abstract Optional<Path> determinePath();
protected abstract Optional<Path> determineInstallation();
private Optional<Path> determineFromPath() {
// Try to locate if it is in the Path
try (var cc = LocalStore.getShell()
.command(ShellDialects.getPlatformDefault().getWhichCommand("code.cmd"))
.start()) {
var out = cc.readStdoutDiscardErr();
var exit = cc.getExitCode();
if (exit == 0) {
var first = out.lines().findFirst();
if (first.isPresent()) {
return first.map(Path::of);
}
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).omit().handle();
}
return Optional.empty();
}
@Override
public boolean isSelectable() {
@ -105,8 +144,13 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue {
@Override
public boolean isAvailable() {
var path = determinePath();
return path.isPresent() && Files.exists(path.get());
var path = determineFromPath();
if (path.isPresent() && Files.exists(path.get())) {
return true;
}
var installation = determineInstallation();
return installation.isPresent() && Files.exists(installation.get());
}
}
}

View file

@ -1,30 +1,28 @@
package io.xpipe.app.prefs;
import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.ApplicationHelper;
import io.xpipe.app.util.WindowsRegistry;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellDialects;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.function.Supplier;
public interface ExternalEditorType extends PrefsChoiceValue {
ExternalEditorType NOTEPAD = new WindowsFullPathType("app.notepad") {
ExternalEditorType NOTEPAD = new WindowsType("app.notepad", "notepad") {
@Override
protected Optional<Path> determinePath() {
protected Optional<Path> determineInstallation() {
return Optional.of(Path.of(System.getenv("SystemRoot") + "\\System32\\notepad.exe"));
}
};
ExternalEditorType VSCODE_WINDOWS = new WindowsFullPathType("app.vscode") {
ExternalEditorType VSCODE_WINDOWS = new WindowsType("app.vscode", "code.cmd") {
@Override
public boolean canOpenDirectory() {
@ -32,23 +30,7 @@ public interface ExternalEditorType extends PrefsChoiceValue {
}
@Override
protected Optional<Path> determinePath() {
// Try to locate if it is in the Path
try (var cc = LocalStore.getShell()
.command(ShellDialects.getPlatformDefault().getWhichCommand("code.cmd"))
.start()) {
var out = cc.readStdoutDiscardErr();
var exit = cc.getExitCode();
if (exit == 0) {
var first = out.lines().findFirst();
if (first.isPresent()) {
return first.map(Path::of);
}
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).omit().handle();
}
protected Optional<Path> determineInstallation() {
return Optional.of(Path.of(System.getenv("LOCALAPPDATA"))
.resolve("Programs")
.resolve("Microsoft VS Code")
@ -61,10 +43,10 @@ public interface ExternalEditorType extends PrefsChoiceValue {
return false;
}
};
ExternalEditorType NOTEPADPLUSPLUS_WINDOWS = new WindowsFullPathType("app.notepad++") {
ExternalEditorType NOTEPADPLUSPLUS_WINDOWS = new WindowsType("app.notepad++", "notepad++") {
@Override
protected Optional<Path> determinePath() {
protected Optional<Path> determineInstallation() {
Optional<String> launcherDir;
launcherDir = WindowsRegistry.readString(WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\Notepad++", null)
.map(p -> p + "\\notepad++.exe");
@ -137,8 +119,8 @@ public interface ExternalEditorType extends PrefsChoiceValue {
throw new IllegalStateException("No custom editor command specified");
}
var format = customCommand.contains("$file") ? customCommand : customCommand + " $file";
ApplicationHelper.executeLocalApplication(sc -> ApplicationHelper.replaceFileArgument(format, "file", file.toString()), true);
var format = customCommand.toLowerCase(Locale.ROOT).contains("$file") ? customCommand : customCommand + " $FILE";
ApplicationHelper.executeLocalApplication(sc -> ApplicationHelper.replaceFileArgument(format, "FILE", file.toString()), true);
}
@Override
@ -175,11 +157,14 @@ public interface ExternalEditorType extends PrefsChoiceValue {
}
}
abstract class WindowsFullPathType extends ExternalApplicationType.WindowsFullPathType
abstract class WindowsType extends ExternalApplicationType.WindowsType
implements ExternalEditorType {
public WindowsFullPathType(String id) {
super(id);
private final String executable;
public WindowsType(String id, String executable) {
super(id, executable);
this.executable = executable;
}
public boolean detach() {
@ -188,7 +173,7 @@ public interface ExternalEditorType extends PrefsChoiceValue {
@Override
public void launch(Path file) throws Exception {
var path = determinePath();
var path = determineInstallation();
if (path.isEmpty()) {
throw new IOException("Unable to find installation of " + toTranslatedString());
}

View file

@ -17,6 +17,7 @@ import lombok.Getter;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.stream.Stream;
@ -25,8 +26,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
ExternalTerminalType CMD = new SimplePathType("app.cmd", "cmd.exe") {
@Override
protected String toCommand(String name, String file) {
return "/C \"" + file + "\"";
protected CommandBuilder toCommand(String name, String file) {
return CommandBuilder.of().add("/C").addFile(file);
}
@Override
@ -38,8 +39,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
ExternalTerminalType POWERSHELL_WINDOWS = new SimplePathType("app.powershell", "powershell") {
@Override
protected String toCommand(String name, String file) {
return "-ExecutionPolicy Bypass -NoProfile -Command cmd /C '" + file + "'";
protected CommandBuilder toCommand(String name, String file) {
return CommandBuilder.of().add("-ExecutionPolicy", "Bypass", "-NoProfile", "-Command", "cmd", "/C").addFile(file);
}
@Override
@ -51,10 +52,12 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
ExternalTerminalType PWSH_WINDOWS = new SimplePathType("app.pwsh", "pwsh") {
@Override
protected String toCommand(String name, String file) {
protected CommandBuilder toCommand(String name, String file) {
// Fix for https://github.com/PowerShell/PowerShell/issues/18530#issuecomment-1325691850
var script = ScriptHelper.createLocalExecScript("set \"PSModulePath=\"\r\n\"" + file + "\"\npause");
return "-ExecutionPolicy Bypass -NoProfile -Command cmd /C '" + script + "'";
return CommandBuilder.of().add("-ExecutionPolicy", "Bypass", "-NoProfile", "-Command", "cmd", "/C").add(sc -> {
var script = ScriptHelper.createLocalExecScript("set \"PSModulePath=\"\r\n\"" + file + "\"\npause");
return sc.getShellDialect().fileArgument(script);
});
}
@Override
@ -66,12 +69,12 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
ExternalTerminalType WINDOWS_TERMINAL = new SimplePathType("app.windowsTerminal", "wt.exe") {
@Override
protected String toCommand(String name, String file) {
protected CommandBuilder toCommand(String name, String file) {
// A weird behavior in Windows Terminal causes the trailing
// backslash of a filepath to escape the closing quote in the title argument
// So just remove that slash
var fixedName = FileNames.removeTrailingSlash(name);
return "-w 1 nt --title \"" + fixedName + "\" \"" + file + "\"";
return CommandBuilder.of().add("-w", "1", "nt", "--title").addQuoted(fixedName).addFile(file);
}
@Override
@ -83,15 +86,14 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
ExternalTerminalType ALACRITTY_WINDOWS = new SimplePathType("app.alacrittyWindows", "alacritty") {
@Override
protected String toCommand(String name, String file) {
protected CommandBuilder toCommand(String name, String file) {
return CommandBuilder.of()
.add("-t")
.addQuoted(name)
.add("-e")
.add("cmd")
.add("/c")
.addQuoted(file.replaceAll(" ", "^$0"))
.build();
.addQuoted(file.replaceAll(" ", "^$0"));
}
@Override
@ -99,16 +101,16 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
return OsType.getLocal().equals(OsType.WINDOWS);
}
};
abstract class WindowsFullPathType extends ExternalApplicationType.WindowsFullPathType
abstract class WindowsType extends ExternalApplicationType.WindowsType
implements ExternalTerminalType {
public WindowsFullPathType(String id) {
super(id);
public WindowsType(String id, String executable) {
super(id, executable);
}
@Override
public void launch(String name, String file, boolean elevated) throws Exception {
var path = determinePath();
var path = determineInstallation();
if (path.isEmpty()) {
throw new IOException("Unable to find installation of " + toTranslatedString());
}
@ -120,7 +122,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
protected abstract String createCommand(ShellControl shellControl, String name, String path, String file);
}
ExternalTerminalType TABBY_WINDOWS = new WindowsFullPathType("app.tabbyWindows") {
ExternalTerminalType TABBY_WINDOWS = new WindowsType("app.tabbyWindows", "tabby") {
@Override
protected String createCommand(ShellControl shellControl, String name, String path, String file) {
@ -129,7 +131,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
}
@Override
protected Optional<Path> determinePath() {
protected Optional<Path> determineInstallation() {
Optional<String> launcherDir;
launcherDir = WindowsRegistry.readString(
WindowsRegistry.HKEY_CURRENT_USER,
@ -167,7 +169,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
try (ShellControl pc = LocalStore.getShell()) {
ApplicationHelper.checkSupport(pc, executable, toTranslatedString(), null);
var toExecute = executable + " " + toCommand(name, file);
var toExecute = executable + " " + toCommand(name, file).build(pc);
// In order to fix this bug which also affects us:
// https://askubuntu.com/questions/1148475/launching-gnome-terminal-from-vscode
toExecute = "GNOME_TERMINAL_SCREEN=\"\" nohup " + toExecute + " </dev/null &>/dev/null & disown";
@ -176,8 +178,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
}
@Override
protected String toCommand(String name, String file) {
return "-v --title \"" + name + "\" -- \"" + file + "\"";
protected CommandBuilder toCommand(String name, String file) {
return CommandBuilder.of().add("-v", "--title").addQuoted(name).add("--").addFile(file);
}
@Override
@ -189,11 +191,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
ExternalTerminalType KONSOLE = new SimplePathType("app.konsole", "konsole") {
@Override
protected String toCommand(String name, String file) {
protected CommandBuilder toCommand(String name, String file) {
// Note for later: When debugging konsole launches, it will always open as a child process of
// IntelliJ/XPipe even though we try to detach it.
// This is not the case for production where it works as expected
return "--new-tab -e \"" + file + "\"";
return CommandBuilder.of().add("--new-tab", "-e").add("--").addFile(file);
}
@Override
@ -205,8 +207,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
ExternalTerminalType XFCE = new SimplePathType("app.xfce", "xfce4-terminal") {
@Override
protected String toCommand(String name, String file) {
return "--tab --title \"" + name + "\" --command \"" + file + "\"";
protected CommandBuilder toCommand(String name, String file) {
return CommandBuilder.of().add("--tab", "--title").addQuoted(name).add("--command").addFile(file);
}
@Override
@ -218,14 +220,13 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
ExternalTerminalType TERMINATOR = new SimplePathType("app.terminator", "terminator") {
@Override
protected String toCommand(String name, String file) {
protected CommandBuilder toCommand(String name, String file) {
return CommandBuilder.of()
.add("-e")
.addQuoted(file)
.add("-T")
.addQuoted(name)
.add("--new-tab")
.build();
.add("--new-tab");
}
@Override
@ -237,8 +238,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
ExternalTerminalType KITTY = new SimplePathType("app.kitty", "kitty") {
@Override
protected String toCommand(String name, String file) {
return CommandBuilder.of().add("-T").addQuoted(name).addQuoted(file).build();
protected CommandBuilder toCommand(String name, String file) {
return CommandBuilder.of().add("-T").addQuoted(name).addQuoted(file);
}
@Override
@ -250,14 +251,13 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
ExternalTerminalType TERMINOLOGY = new SimplePathType("app.terminology", "terminology") {
@Override
protected String toCommand(String name, String file) {
protected CommandBuilder toCommand(String name, String file) {
return CommandBuilder.of()
.add("-T")
.addQuoted(name)
.add("-2")
.add("-e")
.addQuoted(file)
.build();
.addQuoted(file);
}
@Override
@ -269,13 +269,12 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
ExternalTerminalType COOL_RETRO_TERM = new SimplePathType("app.coolRetroTerm", "cool-retro-term") {
@Override
protected String toCommand(String name, String file) {
protected CommandBuilder toCommand(String name, String file) {
return CommandBuilder.of()
.add("-T")
.addQuoted(name)
.add("-e")
.addQuoted(file)
.build();
.addQuoted(file);
}
@Override
@ -287,13 +286,12 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
ExternalTerminalType GUAKE = new SimplePathType("app.guake", "guake") {
@Override
protected String toCommand(String name, String file) {
protected CommandBuilder toCommand(String name, String file) {
return CommandBuilder.of()
.add("-r")
.addQuoted(name)
.add("-e")
.addQuoted(file)
.build();
.addQuoted(file);
}
@Override
@ -305,13 +303,12 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
ExternalTerminalType ALACRITTY = new SimplePathType("app.alacritty", "alacritty") {
@Override
protected String toCommand(String name, String file) {
protected CommandBuilder toCommand(String name, String file) {
return CommandBuilder.of()
.add("-t")
.addQuoted(name)
.add("-e")
.addQuoted(file)
.build();
.addQuoted(file);
}
@Override
@ -323,8 +320,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
ExternalTerminalType TILDA = new SimplePathType("app.tilda", "tilda") {
@Override
protected String toCommand(String name, String file) {
return CommandBuilder.of().add("-c").addQuoted(file).build();
protected CommandBuilder toCommand(String name, String file) {
return CommandBuilder.of().add("-c").addQuoted(file);
}
@Override
@ -450,9 +447,9 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
throw new IllegalStateException("No custom terminal command specified");
}
var format = custom.contains("$cmd") ? custom : custom + " $cmd";
var format = custom.toLowerCase(Locale.ROOT).contains("$cmd") ? custom : custom + " $CMD";
try (var pc = LocalStore.getShell()) {
var toExecute = ApplicationHelper.replaceFileArgument(format, "cmd", file);
var toExecute = ApplicationHelper.replaceFileArgument(format, "CMD", file);
if (pc.getOsType().equals(OsType.WINDOWS)) {
toExecute = "start \"" + name + "\" " + toExecute;
} else {
@ -579,7 +576,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
.start()) {
ApplicationHelper.checkSupport(pc, executable, toTranslatedString(), null);
var toExecute = "Start-Process \"" + executable + "\" -Verb RunAs -ArgumentList \""
+ toCommand(name, file).replaceAll("\"", "`\"") + "\"";
+ toCommand(name, file).build(pc).replaceAll("\"", "`\"") + "\"";
pc.executeSimpleCommand(toExecute);
}
return;
@ -589,7 +586,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
try (ShellControl pc = LocalStore.getShell()) {
ApplicationHelper.checkSupport(pc, executable, toTranslatedString(), null);
var toExecute = executable + " " + toCommand(name, file);
var toExecute = executable + " " + toCommand(name, file).build(pc);
if (pc.getOsType().equals(OsType.WINDOWS)) {
toExecute = "start \"" + name + "\" " + toExecute;
} else {
@ -599,7 +596,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
}
}
protected abstract String toCommand(String name, String file);
protected abstract CommandBuilder toCommand(String name, String file);
public boolean isAvailable() {
try (ShellControl pc = LocalStore.getShell()) {

View file

@ -4,6 +4,7 @@ import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.source.DataStoreId;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.FixedHierarchyStore;
import lombok.Getter;
@ -111,6 +112,10 @@ public abstract class DataStorage {
}
public synchronized List<DataStoreEntry> getStoreChildren(DataStoreEntry entry, boolean display, boolean deep) {
if (!entry.getState().isUsable()) {
return List.of();
}
var children = new ArrayList<>(getStoreEntries().stream()
.filter(other -> {
if (!other.getState().isUsable()) {
@ -180,6 +185,18 @@ public abstract class DataStorage {
return entry;
}
public DataStoreId getId(DataStoreEntry entry) {
var names = new ArrayList<String>();
names.add(entry.getName());
DataStoreEntry current = entry;
while ((current = getParent(current, false).orElse(null)) != null) {
names.add(0, current.getName());
}
return DataStoreId.create(names.toArray(String[]::new));
}
public synchronized DataStoreEntry getStoreEntry(@NonNull DataStore store) {
var entry = storeEntries.stream()
.filter(n -> n.getStore() != null && Objects.equals(store.getClass(), n.getStore().getClass()) && store.equals(n.getStore()))
@ -188,6 +205,25 @@ public abstract class DataStorage {
return entry;
}
public synchronized Optional<DataStoreEntry> getStoreEntryIfPresent(@NonNull DataStoreId id) {
var current = getStoreEntryIfPresent(id.getNames().get(0));
if (current.isPresent()) {
for (int i = 1; i < id.getNames().size(); i++) {
var children = getStoreChildren(current.get(), false, false);
int finalI = i;
current = children.stream().filter(dataStoreEntry -> dataStoreEntry.getName().equalsIgnoreCase(id.getNames().get(finalI))).findFirst();
if (current.isEmpty()) {
break;
}
}
if (current.isPresent()) {
return current;
}
}
return Optional.empty();
}
public synchronized Optional<DataStoreEntry> getStoreEntryIfPresent(@NonNull DataStore store) {
var entry =
storeEntries.stream().filter(n -> store.equals(n.getStore())).findFirst();

View file

@ -100,11 +100,11 @@ public class StandardStorage extends DataStorage {
storeEntries.forEach(dataStoreEntry -> dataStoreEntry.simpleRefresh());
// Remove even incomplete stores when in production
if (!AppPrefs.get().isDevelopmentEnvironment()) {
storeEntries.removeIf(entry -> {
return !entry.getState().isUsable();
});
}
// if (!AppPrefs.get().isDevelopmentEnvironment()) {
// storeEntries.removeIf(entry -> {
// return !entry.getState().isUsable();
// });
// }
}
} catch (IOException ex) {
ErrorEvent.fromThrowable(ex).terminal(true).build().handle();

View file

@ -203,6 +203,14 @@ public abstract class UpdateHandler {
return;
}
// Check if prepared update is still the latest.
// We only do that here to minimize the sent requests by only executing when it's really necessary
var available = XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheckSilent();
if (available != null && !available.getVersion().equals(preparedUpdate.getValue().getVersion())) {
preparedUpdate.setValue(null);
return;
}
event("Executing update ...");
OperationMode.executeAfterShutdown(() -> {
try {

View file

@ -5,11 +5,16 @@ import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.process.ShellControl;
import java.io.IOException;
import java.util.Locale;
import java.util.function.Function;
public class ApplicationHelper {
public static String replaceFileArgument(String format, String variable, String file) {
// Support for legacy variables that were not upper case
variable = variable.toUpperCase(Locale.ROOT);
format = format.replace("$" + variable.toLowerCase(Locale.ROOT), "$" + variable.toUpperCase(Locale.ROOT));
var fileString = file.contains(" ") ? "\"" + file + "\"" : file;
// Check if the variable is already quoted
var replaced = format.replace("\"$" + variable + "\"", fileString).replace("$" + variable, fileString);

View file

@ -0,0 +1,62 @@
package io.xpipe.app.util;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.app.fxcomps.impl.SecretFieldComp;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.SecretValue;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Alert;
import javafx.scene.layout.StackPane;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class AskpassAlert {
private static final Map<UUID, UUID> requestToId = new HashMap<>();
private static final Map<UUID, SecretValue> passwords = new HashMap<>();
public static SecretValue query(String prompt, DataStore store) {
var rid = UUID.randomUUID();
var secretId = UUID.nameUUIDFromBytes(ByteBuffer.allocate(4).putInt(store.hashCode()).array());
return query(prompt, rid, secretId);
}
public static SecretValue query(String prompt, UUID requestId, UUID secretId) {
if (requestToId.containsKey(requestId)) {
var id = requestToId.remove(requestId);
passwords.remove(id);
}
if (passwords.containsKey(secretId)) {
return passwords.get(secretId);
}
var prop = new SimpleObjectProperty<SecretValue>();
var r = AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("askpassAlertTitle"));
alert.setHeaderText(prompt);
alert.setAlertType(Alert.AlertType.CONFIRMATION);
var text = new SecretFieldComp(prop).createRegion();
alert.getDialogPane().setContent(new StackPane(text));
})
.filter(b -> b.getButtonData().isDefaultButton() && prop.getValue() != null)
.map(t -> {
// AppCache.update(msg.getId(), prop.getValue());
return prop.getValue();
})
.orElse(null);
// If the result is null, assume that the operation was aborted by the user
if (r != null) {
passwords.put(secretId, r);
requestToId.put(requestId, secretId);
}
return r;
}
}

View file

@ -1,12 +1,22 @@
package io.xpipe.app.util;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.process.OsType;
import java.awt.*;
import java.nio.file.Path;
public class DesktopHelper {
public static Path getDesktopDirectory() throws Exception {
if (OsType.getLocal() == OsType.WINDOWS) {
return Path.of(LocalStore.getLocalPowershell().executeSimpleStringCommand("[Environment]::GetFolderPath([Environment+SpecialFolder]::Desktop)"));
}
return Path.of(System.getProperty("user.home") + "/Desktop");
}
public static void browsePath(Path file) {
if (!Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
return;

View file

@ -5,22 +5,22 @@ import io.xpipe.core.process.OsType;
import io.xpipe.core.util.XPipeInstallation;
import java.nio.file.Files;
import java.nio.file.Path;
public class DesktopShortcuts {
private static void createWindowsShortcut(String target, String name) throws Exception {
var icon = XPipeInstallation.getLocalDefaultInstallationIcon();
var shortcutTarget = XPipeInstallation.getLocalDefaultCliExecutable();
var shortcutPath = DesktopHelper.getDesktopDirectory().resolve(name + ".lnk");
var content = String.format(
"""
set "TARGET=%s"
set "SHORTCUT=%%HOMEDRIVE%%%%HOMEPATH%%\\Desktop\\%s.lnk"
set "SHORTCUT=%s"
set PWS=powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile
%%PWS%% -Command "$ws = New-Object -ComObject WScript.Shell; $s = $ws.CreateShortcut('%%SHORTCUT%%'); $S.IconLocation='%s'; $S.WindowStyle=7; $S.TargetPath = '%%TARGET%%'; $S.Arguments = 'open %s'; $S.Save()"
""",
shortcutTarget, name, icon, target);
shortcutTarget, shortcutPath, icon, target);
LocalStore.getShell().executeSimpleCommand(content);
}
@ -39,7 +39,7 @@ public class DesktopShortcuts {
Categories=Utility;Development;Office;
""",
name, exec, target, icon);
var file = Path.of(System.getProperty("user.home") + "/Desktop/" + name + ".desktop");
var file = DesktopHelper.getDesktopDirectory().resolve(name + ".desktop");
Files.writeString(file, content);
file.toFile().setExecutable(true);
}
@ -47,7 +47,7 @@ public class DesktopShortcuts {
private static void createMacOSShortcut(String target, String name) throws Exception {
var exec = XPipeInstallation.getLocalDefaultCliExecutable();
var icon = XPipeInstallation.getLocalDefaultInstallationIcon();
var base = System.getProperty("user.home") + "/Desktop/" + name + ".app";
var base = DesktopHelper.getDesktopDirectory().resolve(name + ".app");
var content = String.format(
"""
#!/bin/bash

View file

@ -2,9 +2,11 @@ package io.xpipe.app.util;
import atlantafx.base.controls.Spacer;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.GuiDialog;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.impl.*;
import io.xpipe.core.util.SecretValue;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
@ -14,13 +16,13 @@ import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import net.synedra.validatorfx.Check;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.function.Supplier;
public class OptionsBuilder {
private final Validator ownValidator;
private final List<Validator> allValidators = new ArrayList<>();
private final List<OptionsComp.Entry> entries = new ArrayList<>();
private final List<Property<?>> props = new ArrayList<>();
@ -29,6 +31,47 @@ public class OptionsBuilder {
private String longDescription;
private Comp<?> comp;
public Validator buildEffectiveValidator() {
return new ChainedValidator(allValidators);
}
public OptionsBuilder() {
this.ownValidator = new SimpleValidator();
this.allValidators.add(ownValidator);
}
public OptionsBuilder(Validator validator) {
this.ownValidator = validator;
this.allValidators.add(ownValidator);
}
public OptionsBuilder choice(IntegerProperty selectedIndex, Map<String, OptionsBuilder> options) {
var list = options.entrySet().stream()
.map(e -> new ChoicePaneComp.Entry(
AppI18n.observable(e.getKey()), e.getValue().buildComp()))
.toList();
var validatorList =
options.values().stream().map(builder -> builder.buildEffectiveValidator()).toList();
var selected = new SimpleObjectProperty<>(list.get(selectedIndex.getValue()));
selected.addListener((observable, oldValue, newValue) -> {
selectedIndex.setValue(newValue != null ? list.indexOf(newValue) : null);
});
var pane = new ChoicePaneComp(list, selected);
var validatorMap = new LinkedHashMap<ChoicePaneComp.Entry, Validator>();
for (int i = 0; i < list.size(); i++) {
validatorMap.put(list.get(i), validatorList.get(i));
}
validatorMap.put(null, new SimpleValidator());
var orVal = new ExclusiveValidator<>(validatorMap, selected);
options.values().forEach(builder -> props.addAll(builder.props));
props.add(selectedIndex);
allValidators.add(orVal);
pushComp(pane);
return this;
}
private void finishCurrent() {
if (comp == null) {
return;
@ -44,10 +87,20 @@ public class OptionsBuilder {
public OptionsBuilder sub(OptionsBuilder builder) {
props.addAll(builder.props);
allValidators.add(builder.buildEffectiveValidator());
pushComp(builder.buildComp());
return this;
}
public OptionsBuilder sub(OptionsBuilder builder, Property<?> prop) {
props.addAll(builder.props);
allValidators.add(builder.buildEffectiveValidator());
props.add(prop);
pushComp(builder.buildComp());
return this;
}
public OptionsBuilder addTitle(String titleKey) {
finishCurrent();
entries.add(new OptionsComp.Entry(
@ -76,6 +129,12 @@ public class OptionsBuilder {
return this;
}
public OptionsBuilder nonNull() {
var e = name;
var p = props.get(props.size() - 1);
return decorate(Validator.nonNull(ownValidator, e, p));
}
public OptionsBuilder nonNull(Validator v) {
var e = name;
var p = props.get(props.size() - 1);
@ -130,7 +189,6 @@ public class OptionsBuilder {
return addComp(Comp.separator());
}
public OptionsBuilder name(String nameKey) {
finishCurrent();
name = AppI18n.observable(nameKey);
@ -143,6 +201,12 @@ public class OptionsBuilder {
return this;
}
public OptionsBuilder description(ObservableValue<String> description) {
finishCurrent();
this.description = description;
return this;
}
public OptionsBuilder longDescription(String descriptionKey) {
finishCurrent();
longDescription = AppI18n.getInstance().getMarkdownDocumentation(descriptionKey);
@ -207,4 +271,8 @@ public class OptionsBuilder {
public Region build() {
return buildComp().createRegion();
}
public GuiDialog buildDialog() {
return new GuiDialog(buildComp(), buildEffectiveValidator());
}
}

View file

@ -1,8 +1,14 @@
package io.xpipe.app.util;
import io.xpipe.app.issue.TrackEvent;
import javafx.application.Platform;
import lombok.Getter;
import lombok.Setter;
import java.awt.*;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
public enum PlatformState {
NOT_INITIALIZED,
RUNNING,
@ -11,4 +17,74 @@ public enum PlatformState {
@Getter
@Setter
private static PlatformState current = PlatformState.NOT_INITIALIZED;
public static boolean HAS_GRAPHICS;
public static boolean PLATFORM_LOADED;
public static void teardown() {
Platform.exit();
setCurrent(PlatformState.EXITED);
}
public static void initPlatformOrThrow() throws Throwable {
var r = PlatformState.initPlatform();
if (r.isPresent()) {
throw r.get();
}
}
public static Optional<Throwable> initPlatform() {
if (current == EXITED) {
return Optional.of(new IllegalStateException("Platform has already exited"));
}
if (current == RUNNING) {
return Optional.empty();
}
try {
GraphicsDevice[] screenDevices =
GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();
HAS_GRAPHICS = screenDevices != null && screenDevices.length > 0;
} catch (HeadlessException e) {
TrackEvent.warn(e.getMessage());
HAS_GRAPHICS = false;
return Optional.of(e);
}
try {
CountDownLatch latch = new CountDownLatch(1);
Platform.setImplicitExit(false);
Platform.startup(latch::countDown);
try {
latch.await();
PLATFORM_LOADED = true;
PlatformState.setCurrent(PlatformState.RUNNING);
return Optional.empty();
} catch (InterruptedException e) {
return Optional.of(e);
}
} catch (Throwable t) {
// Check if we already exited
if ("Platform.exit has been called".equals(t.getMessage())) {
PLATFORM_LOADED = true;
PlatformState.setCurrent(PlatformState.EXITED);
return Optional.of(t);
}
else if ("Toolkit already initialized".equals(t.getMessage())) {
PLATFORM_LOADED = true;
PlatformState.setCurrent(PlatformState.RUNNING);
return Optional.empty();
}
else {
// Platform initialization has failed in this case
PLATFORM_LOADED = false;
PlatformState.setCurrent(PlatformState.EXITED);
return Optional.of(t);
}
}
}
}

View file

@ -44,10 +44,13 @@ public class ScriptHelper {
}
}
public static String constructInitFile(
ShellControl processControl, List<String> init, String toExecuteInShell, boolean login, String displayName)
public static String constructInitFile(ShellControl processControl, List<String> init, String toExecuteInShell, boolean login, String displayName)
throws Exception {
return constructInitFile(processControl.getShellDialect(), processControl, init, toExecuteInShell, login, displayName);
}
public static String constructInitFile(ShellDialect t, ShellControl processControl, List<String> init, String toExecuteInShell, boolean login, String displayName)
throws Exception {
ShellDialect t = processControl.getShellDialect();
String nl = t.getNewLine().getNewLineString();
var content = String.join(nl, init.stream().filter(s -> s != null).toList()) + nl;
@ -73,7 +76,7 @@ public class ScriptHelper {
content += t.getExitCommand() + nl;
}
var initFile = createExecScript(processControl, t.initFileName(processControl), content);
var initFile = createExecScript(t, processControl, t.initFileName(processControl), content);
return initFile;
}
@ -97,12 +100,11 @@ public class ScriptHelper {
ShellDialect type = processControl.getShellDialect();
var temp = processControl.getSubTemporaryDirectory();
var file = FileNames.join(temp, fileName + "." + type.getScriptFileEnding());
return createExecScript(processControl, file, content);
return createExecScript(processControl.getShellDialect(), processControl, file, content);
}
@SneakyThrows
public static String createExecScript(ShellControl processControl, String file, String content) {
ShellDialect type = processControl.getShellDialect();
public static String createExecScript(ShellDialect type, ShellControl processControl, String file, String content) {
content = type.prepareScriptContent(content);
TrackEvent.withTrace("proc", "Writing exec script")
@ -114,7 +116,7 @@ public class ScriptHelper {
.getShellDialect()
.createScriptTextFileWriteCommand(processControl, content, file)
.execute();
var e = type.getScriptPermissionsCommand(file);
var e = processControl.getShellDialect().getScriptPermissionsCommand(file);
if (e != null) {
processControl.executeSimpleCommand(e, "Failed to set script permissions of " + file);
}
@ -152,7 +154,7 @@ public class ScriptHelper {
pass.stream()
.map(secretValue -> secretValue.getSecretValue())
.toList());
var exec = createExecScript(sub, file, content);
var exec = createExecScript(sub.getShellDialect(), sub, file, content);
return exec;
}
} else {
@ -163,7 +165,7 @@ public class ScriptHelper {
pass.stream()
.map(secretValue -> secretValue.getSecretValue())
.toList());
var exec = createExecScript(parent, file, content);
var exec = createExecScript(parent.getShellDialect(), parent, file, content);
return exec;
}
}

View file

@ -0,0 +1,130 @@
package io.xpipe.app.util;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.impl.LocalStore;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.SecretValue;
import lombok.Builder;
import lombok.Getter;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.util.function.Supplier;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = SecretRetrievalStrategy.None.class),
@JsonSubTypes.Type(value = SecretRetrievalStrategy.Unsupported.class),
@JsonSubTypes.Type(value = SecretRetrievalStrategy.Reference.class),
@JsonSubTypes.Type(value = SecretRetrievalStrategy.InPlace.class),
@JsonSubTypes.Type(value = SecretRetrievalStrategy.Prompt.class),
@JsonSubTypes.Type(value = SecretRetrievalStrategy.CustomCommand.class),
@JsonSubTypes.Type(value = SecretRetrievalStrategy.PasswordManager.class)
})
public interface SecretRetrievalStrategy {
SecretValue retrieve(String displayName, DataStore store) throws Exception;
@JsonTypeName("none")
public static class None implements SecretRetrievalStrategy {
@Override
public SecretValue retrieve(String displayName, DataStore store) {
return null;
}
}
@JsonTypeName("unsupported")
public static class Unsupported implements SecretRetrievalStrategy {
@Override
public SecretValue retrieve(String displayName, DataStore store) {
throw new UnsupportedOperationException();
}
}
@JsonTypeName("reference")
public static class Reference implements SecretRetrievalStrategy {
@JsonIgnore
private final Supplier<SecretValue> supplier;
public Reference(Supplier<SecretValue> supplier) {
this.supplier = supplier;
}
@Override
public SecretValue retrieve(String displayName, DataStore store) {
return supplier.get();
}
}
@JsonTypeName("inPlace")
@Getter
@Builder
@Value
@Jacksonized
public static class InPlace implements SecretRetrievalStrategy {
SecretValue value;
public InPlace(SecretValue value) {
this.value = value;
}
@Override
public SecretValue retrieve(String displayName, DataStore store) {
return value;
}
}
@JsonTypeName("prompt")
public static class Prompt implements SecretRetrievalStrategy {
@Override
public SecretValue retrieve(String displayName, DataStore store) {
return AskpassAlert.query(displayName, store);
}
}
@JsonTypeName("passwordManager")
@Builder
@Jacksonized
@Value
public static class PasswordManager implements SecretRetrievalStrategy {
String key;
@Override
public SecretValue retrieve(String displayName, DataStore store) throws Exception {
var cmd = AppPrefs.get().passwordManagerString(key);
if (cmd == null) {
return null;
}
try (var cc = new LocalStore().createBasicControl().command(cmd).start()) {
return SecretHelper.encrypt(cc.readStdoutOrThrow());
}
}
}
@JsonTypeName("customCommand")
@Builder
@Jacksonized
@Value
public static class CustomCommand implements SecretRetrievalStrategy {
String command;
@Override
public SecretValue retrieve(String displayName, DataStore store) throws Exception {
try (var cc = new LocalStore().createBasicControl().command(command).start()) {
return SecretHelper.encrypt(cc.readStdoutOrThrow());
}
}
}
}

View file

@ -0,0 +1,105 @@
package io.xpipe.app.util;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.App;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.SecretFieldComp;
import io.xpipe.app.fxcomps.impl.TextFieldComp;
import io.xpipe.app.prefs.AppPrefs;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.LinkedHashMap;
import java.util.List;
public class SecretRetrievalStrategyHelper {
private static OptionsBuilder inPlace(Property<SecretRetrievalStrategy.InPlace> p) {
var secretProperty =
new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getValue() : null);
return new OptionsBuilder()
.name("password")
.addComp(new SecretFieldComp(secretProperty), secretProperty)
.bind(
() -> {
return new SecretRetrievalStrategy.InPlace(secretProperty.getValue());
},
p);
}
private static OptionsBuilder passwordManager(Property<SecretRetrievalStrategy.PasswordManager> p) {
var keyProperty =
new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getKey() : null);
var content = new HorizontalComp(List.<Comp<?>>of(
new TextFieldComp(keyProperty).hgrow(),
new ButtonComp(null, new FontIcon("mdomz-settings"), () -> {
AppPrefs.get().selectCategory(3);
App.getApp().getStage().requestFocus();
})
.grow(false, true)))
.apply(struc -> struc.get().setSpacing(10));
return new OptionsBuilder()
.name("command")
.addComp(content, keyProperty)
.bind(
() -> {
return new SecretRetrievalStrategy.PasswordManager(keyProperty.getValue());
},
p);
}
private static OptionsBuilder customCommand(Property<SecretRetrievalStrategy.CustomCommand> p) {
var cmdProperty =
new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getCommand() : null);
var content = new TextFieldComp(cmdProperty).apply(struc -> struc.get().setPromptText("Password key"));
return new OptionsBuilder()
.name("command")
.addComp(content, cmdProperty)
.bind(
() -> {
return new SecretRetrievalStrategy.CustomCommand(cmdProperty.getValue());
},
p);
}
public static OptionsBuilder comp(Property<SecretRetrievalStrategy> s) {
SecretRetrievalStrategy strat = s.getValue();
var inPlace = new SimpleObjectProperty<>(strat instanceof SecretRetrievalStrategy.InPlace i ? i : null);
var passwordManager =
new SimpleObjectProperty<>(strat instanceof SecretRetrievalStrategy.PasswordManager i ? i : null);
var customCommand =
new SimpleObjectProperty<>(strat instanceof SecretRetrievalStrategy.CustomCommand i ? i : null);
var command = new SimpleObjectProperty<>(strat instanceof SecretRetrievalStrategy.CustomCommand c ? c : null);
var map = new LinkedHashMap<String, OptionsBuilder>();
map.put("none", new OptionsBuilder());
map.put("password", inPlace(inPlace));
map.put("passwordManager", passwordManager(passwordManager));
map.put("customCommand", customCommand(customCommand));
map.put("prompt", new OptionsBuilder());
var selected = new SimpleIntegerProperty(
strat instanceof SecretRetrievalStrategy.None
? 0
: strat instanceof SecretRetrievalStrategy.InPlace
? 1
: strat instanceof SecretRetrievalStrategy.PasswordManager
? 2
: strat instanceof SecretRetrievalStrategy.CustomCommand ? 3 : strat instanceof SecretRetrievalStrategy.Prompt ? 4 : 0);
return new OptionsBuilder()
.choice(selected, map)
.bindChoice(
() -> {
return switch (selected.get()) {
case 0 -> new SimpleObjectProperty<>(new SecretRetrievalStrategy.None());
case 1 -> inPlace;
case 2 -> passwordManager;
case 3 -> customCommand;
case 4 -> new SimpleObjectProperty<>(new SecretRetrievalStrategy.Prompt());
default -> new SimpleObjectProperty<>();
};
},
s);
}
}

View file

@ -6,14 +6,14 @@ import org.apache.commons.lang3.function.FailableRunnable;
public class ThreadHelper {
public static Thread runAsync(Runnable r) {
var t = new Thread(r);
var t = Thread.ofVirtual().unstarted(r);
t.setDaemon(true);
t.start();
return t;
}
public static Thread runFailableAsync(FailableRunnable<Throwable> r) {
var t = new Thread(() -> {
var t = Thread.ofVirtual().unstarted(() -> {
try {
r.run();
} catch (Throwable e) {

View file

@ -14,7 +14,7 @@ public interface Validator {
static Check nonNull(Validator v, ObservableValue<String> name, ObservableValue<?> s) {
return v.createCheck().dependsOn("val", s).withMethod(c -> {
if (c.get("val") == null) {
c.error(AppI18n.get("app.mustNotBeEmpty", name.getValue()));
c.error(AppI18n.get("app.mustNotBeEmpty", name != null ? name.getValue() : "null"));
}
});
}

View file

@ -145,6 +145,8 @@ open module io.xpipe.app {
FocusExchangeImpl,
ProxyReadConnectionExchangeImpl,
StatusExchangeImpl,
DrainExchangeImpl,
SinkExchangeImpl,
StopExchangeImpl,
ModeExchangeImpl,
DialogExchangeImpl,

View file

@ -0,0 +1 @@
<svg fill="#0D597F" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Alpine Linux</title><path d="M5.998 1.607L0 12l5.998 10.393h12.004L24 12 18.002 1.607H5.998zM9.965 7.12L12.66 9.9l1.598 1.595.002-.002 2.41 2.363c-.2.14-.386.252-.563.344a3.756 3.756 0 01-.496.217 2.702 2.702 0 01-.425.111c-.131.023-.25.034-.358.034-.13 0-.242-.014-.338-.034a1.317 1.317 0 01-.24-.072.95.95 0 01-.2-.113l-1.062-1.092-3.039-3.041-1.1 1.053-3.07 3.072a.974.974 0 01-.2.111 1.274 1.274 0 01-.237.073c-.096.02-.209.033-.338.033-.108 0-.227-.009-.358-.031a2.7 2.7 0 01-.425-.114 3.748 3.748 0 01-.496-.217 5.228 5.228 0 01-.563-.343l6.803-6.727zm4.72.785l4.579 4.598 1.382 1.353a5.24 5.24 0 01-.564.344 3.73 3.73 0 01-.494.217 2.697 2.697 0 01-.426.111c-.13.023-.251.034-.36.034-.129 0-.241-.014-.337-.034a1.285 1.285 0 01-.385-.146c-.033-.02-.05-.036-.053-.04l-1.232-1.218-2.111-2.111-.334.334L12.79 9.8l1.896-1.897zm-5.966 4.12v2.529a2.128 2.128 0 01-.356-.035 2.765 2.765 0 01-.422-.116 3.708 3.708 0 01-.488-.214 5.217 5.217 0 01-.555-.34l1.82-1.825Z"/></svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1 @@
<svg fill="#FF9900" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Amazon</title><path d="M.045 18.02c.072-.116.187-.124.348-.022 3.636 2.11 7.594 3.166 11.87 3.166 2.852 0 5.668-.533 8.447-1.595l.315-.14c.138-.06.234-.1.293-.13.226-.088.39-.046.525.13.12.174.09.336-.12.48-.256.19-.6.41-1.006.654-1.244.743-2.64 1.316-4.185 1.726a17.617 17.617 0 01-10.951-.577 17.88 17.88 0 01-5.43-3.35c-.1-.074-.151-.15-.151-.22 0-.047.021-.09.051-.13zm6.565-6.218c0-1.005.247-1.863.743-2.577.495-.71 1.17-1.25 2.04-1.615.796-.335 1.756-.575 2.912-.72.39-.046 1.033-.103 1.92-.174v-.37c0-.93-.105-1.558-.3-1.875-.302-.43-.78-.65-1.44-.65h-.182c-.48.046-.896.196-1.246.46-.35.27-.575.63-.675 1.096-.06.3-.206.465-.435.51l-2.52-.315c-.248-.06-.372-.18-.372-.39 0-.046.007-.09.022-.15.247-1.29.855-2.25 1.82-2.88.976-.616 2.1-.975 3.39-1.05h.54c1.65 0 2.957.434 3.888 1.29.135.15.27.3.405.48.12.165.224.314.283.45.075.134.15.33.195.57.06.254.105.42.135.51.03.104.062.3.076.615.01.313.02.493.02.553v5.28c0 .376.06.72.165 1.036.105.313.21.54.315.674l.51.674c.09.136.136.256.136.36 0 .12-.06.226-.18.314-1.2 1.05-1.86 1.62-1.963 1.71-.165.135-.375.15-.63.045a6.062 6.062 0 01-.526-.496l-.31-.347a9.391 9.391 0 01-.317-.42l-.3-.435c-.81.886-1.603 1.44-2.4 1.665-.494.15-1.093.227-1.83.227-1.11 0-2.04-.343-2.76-1.034-.72-.69-1.08-1.665-1.08-2.94l-.05-.076zm3.753-.438c0 .566.14 1.02.425 1.364.285.34.675.512 1.155.512.045 0 .106-.007.195-.02.09-.016.134-.023.166-.023.614-.16 1.08-.553 1.424-1.178.165-.28.285-.58.36-.91.09-.32.12-.59.135-.8.015-.195.015-.54.015-1.005v-.54c-.84 0-1.484.06-1.92.18-1.275.36-1.92 1.17-1.92 2.43l-.035-.02zm9.162 7.027c.03-.06.075-.11.132-.17.362-.243.714-.41 1.05-.5a8.094 8.094 0 011.612-.24c.14-.012.28 0 .41.03.65.06 1.05.168 1.172.33.063.09.099.228.099.39v.15c0 .51-.149 1.11-.424 1.8-.278.69-.664 1.248-1.156 1.68-.073.06-.14.09-.197.09-.03 0-.06 0-.09-.012-.09-.044-.107-.12-.064-.24.54-1.26.806-2.143.806-2.64 0-.15-.03-.27-.087-.344-.145-.166-.55-.257-1.224-.257-.243 0-.533.016-.87.046-.363.045-.7.09-1 .135-.09 0-.148-.014-.18-.044-.03-.03-.036-.047-.02-.077 0-.017.006-.03.02-.063v-.06z"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1 @@
<svg fill="#FFFFFF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Apple</title><path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"/></svg>

After

Width:  |  Height:  |  Size: 665 B

View file

@ -0,0 +1 @@
<svg fill="#000000" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Apple</title><path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"/></svg>

After

Width:  |  Height:  |  Size: 665 B

View file

@ -0,0 +1 @@
<svg fill="#1793D1" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Arch Linux</title><path d="M11.39.605C10.376 3.092 9.764 4.72 8.635 7.132c.693.734 1.543 1.589 2.923 2.554-1.484-.61-2.496-1.224-3.252-1.86C6.86 10.842 4.596 15.138 0 23.395c3.612-2.085 6.412-3.37 9.021-3.862a6.61 6.61 0 01-.171-1.547l.003-.115c.058-2.315 1.261-4.095 2.687-3.973 1.426.12 2.534 2.096 2.478 4.409a6.52 6.52 0 01-.146 1.243c2.58.505 5.352 1.787 8.914 3.844-.702-1.293-1.33-2.459-1.929-3.57-.943-.73-1.926-1.682-3.933-2.713 1.38.359 2.367.772 3.137 1.234-6.09-11.334-6.582-12.84-8.67-17.74zM22.898 21.36v-.623h-.234v-.084h.562v.084h-.234v.623h.331v-.707h.142l.167.5.034.107a2.26 2.26 0 01.038-.114l.17-.493H24v.707h-.091v-.593l-.206.593h-.084l-.205-.602v.602h-.091"/></svg>

After

Width:  |  Height:  |  Size: 780 B

View file

@ -0,0 +1 @@
<svg fill="#262577" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>CentOS</title><path d="M12.076.066L8.883 3.28H3.348v5.434L0 12.01l3.349 3.298v5.39h5.374l3.285 3.236 3.285-3.236h5.43v-5.374L24 12.026l-3.232-3.252V3.321H15.31zm0 .749l2.49 2.506h-1.69v6.441l-.8.805-.81-.815V3.28H9.627zm-8.2 2.991h4.483L6.485 5.692l4.253 4.279v.654H9.94L5.674 6.423l-1.798 1.77zm5.227 0h1.635v5.415l-3.509-3.53zm4.302.043h1.687l1.83 1.842-3.517 3.539zm2.431 0h4.404v4.394l-1.83-1.842-4.241 4.267h-.764v-.69l4.261-4.287zm2.574 3.3l1.83 1.843v1.676h-5.327zm-12.735.013l3.515 3.462H3.876v-1.69zM3.348 9.454v1.697h6.377l.871.858-.782.77H3.35v1.786L.753 12.01zm17.42.068l2.488 2.503-2.533 2.55v-1.796h-6.41l-.75-.754.825-.83h6.38zm-9.502.978l.81.815.186-.188.614-.618v.686h.768l-.825.83.75.754h-.719v.808l-.842-.83-.741.73v-.707h-.7l.781-.77-.188-.186-.682-.672h.788zm-7.39 2.807h5.402l-3.603 3.55-1.798-1.772zm6.154 0h.708v.7l-4.404 4.338 1.852 1.824h-4.31v-4.342l1.798 1.77zm3.348 0h.715l4.317 4.343.186-.187 1.599-1.61v4.316h-4.366l1.853-1.825-.188-.185-4.116-4.054zm1.46 0h5.357v1.798l-1.785 1.796zm-2.83.191l.842.829v6.37h1.691l-2.532 2.495-2.533-2.495h1.79V14.23zm-1.27 1.251v5.42H8.939l-1.852-1.823zm2.64.097l3.552 3.499-1.853 1.825h-1.7z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1 @@
<svg fill="#A81D33" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Debian</title><path d="M13.88 12.685c-.4 0 .08.2.601.28.14-.1.27-.22.39-.33a3.001 3.001 0 01-.99.05m2.14-.53c.23-.33.4-.69.47-1.06-.06.27-.2.5-.33.73-.75.47-.07-.27 0-.56-.8 1.01-.11.6-.14.89m.781-2.05c.05-.721-.14-.501-.2-.221.07.04.13.5.2.22M12.38.31c.2.04.45.07.42.12.23-.05.28-.1-.43-.12m.43.12l-.15.03.14-.01V.43m6.633 9.944c.02.64-.2.95-.38 1.5l-.35.181c-.28.54.03.35-.17.78-.44.39-1.34 1.22-1.62 1.301-.201 0 .14-.25.19-.34-.591.4-.481.6-1.371.85l-.03-.06c-2.221 1.04-5.303-1.02-5.253-3.842-.03.17-.07.13-.12.2a3.551 3.552 0 012.001-3.501 3.361 3.362 0 013.732.48 3.341 3.342 0 00-2.721-1.3c-1.18.01-2.281.76-2.651 1.57-.6.38-.67 1.47-.93 1.661-.361 2.601.66 3.722 2.38 5.042.27.19.08.21.12.35a4.702 4.702 0 01-1.53-1.16c.23.33.47.66.8.91-.55-.18-1.27-1.3-1.48-1.35.93 1.66 3.78 2.921 5.261 2.3a6.203 6.203 0 01-2.33-.28c-.33-.16-.77-.51-.7-.57a5.802 5.803 0 005.902-.84c.44-.35.93-.94 1.07-.95-.2.32.04.16-.12.44.44-.72-.2-.3.46-1.24l.24.33c-.09-.6.74-1.321.66-2.262.19-.3.2.3 0 .97.29-.74.08-.85.15-1.46.08.2.18.42.23.63-.18-.7.2-1.2.28-1.6-.09-.05-.28.3-.32-.53 0-.37.1-.2.14-.28-.08-.05-.26-.32-.38-.861.08-.13.22.33.34.34-.08-.42-.2-.75-.2-1.08-.34-.68-.12.1-.4-.3-.34-1.091.3-.25.34-.74.54.77.84 1.96.981 2.46-.1-.6-.28-1.2-.49-1.76.16.07-.26-1.241.21-.37A7.823 7.824 0 0017.702 1.6c.18.17.42.39.33.42-.75-.45-.62-.48-.73-.67-.61-.25-.65.02-1.06 0C15.082.73 14.862.8 13.8.4l.05.23c-.77-.25-.9.1-1.73 0-.05-.04.27-.14.53-.18-.741.1-.701-.14-1.431.03.17-.13.36-.21.55-.32-.6.04-1.44.35-1.18.07C9.6.68 7.847 1.3 6.867 2.22L6.838 2c-.45.54-1.96 1.611-2.08 2.311l-.131.03c-.23.4-.38.85-.57 1.261-.3.52-.45.2-.4.28-.6 1.22-.9 2.251-1.16 3.102.18.27 0 1.65.07 2.76-.3 5.463 3.84 10.776 8.363 12.006.67.23 1.65.23 2.49.25-.99-.28-1.12-.15-2.08-.49-.7-.32-.85-.7-1.34-1.13l.2.35c-.971-.34-.57-.42-1.361-.67l.21-.27c-.31-.03-.83-.53-.97-.81l-.34.01c-.41-.501-.63-.871-.61-1.161l-.111.2c-.13-.21-1.52-1.901-.8-1.511-.13-.12-.31-.2-.5-.55l.14-.17c-.35-.44-.64-1.02-.62-1.2.2.24.32.3.45.33-.88-2.172-.93-.12-1.601-2.202l.15-.02c-.1-.16-.18-.34-.26-.51l.06-.6c-.63-.74-.18-3.102-.09-4.402.07-.54.53-1.1.88-1.981l-.21-.04c.4-.71 2.341-2.872 3.241-2.761.43-.55-.09 0-.18-.14.96-.991 1.26-.7 1.901-.88.7-.401-.6.16-.27-.151 1.2-.3.85-.7 2.421-.85.16.1-.39.14-.52.26 1-.49 3.151-.37 4.562.27 1.63.77 3.461 3.011 3.531 5.132l.08.02c-.04.85.13 1.821-.17 2.711l.2-.42M9.54 13.236l-.05.28c.26.35.47.73.8 1.01-.24-.47-.42-.66-.75-1.3m.62-.02c-.14-.15-.22-.34-.31-.52.08.32.26.6.43.88l-.12-.36m10.945-2.382l-.07.15c-.1.76-.34 1.511-.69 2.212.4-.73.65-1.541.75-2.362M12.45.12c.27-.1.66-.05.95-.12-.37.03-.74.05-1.1.1l.15.02M3.006 5.142c.07.57-.43.8.11.42.3-.66-.11-.18-.1-.42m-.64 2.661c.12-.39.15-.62.2-.84-.35.44-.17.53-.2.83"/></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1 @@
<svg fill="#51A2DA" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Fedora</title><path d="M12.001 0C5.376 0 .008 5.369.004 11.992H.002v9.287h.002A2.726 2.726 0 0 0 2.73 24h9.275c6.626-.004 11.993-5.372 11.993-11.997C23.998 5.375 18.628 0 12 0zm2.431 4.94c2.015 0 3.917 1.543 3.917 3.671 0 .197.001.395-.03.619a1.002 1.002 0 0 1-1.137.893 1.002 1.002 0 0 1-.842-1.175 2.61 2.61 0 0 0 .013-.337c0-1.207-.987-1.672-1.92-1.672-.934 0-1.775.784-1.777 1.672.016 1.027 0 2.046 0 3.07l1.732-.012c1.352-.028 1.368 2.009.016 1.998l-1.748.013c-.004.826.006.677.002 1.093 0 0 .015 1.01-.016 1.776-.209 2.25-2.124 4.046-4.424 4.046-2.438 0-4.448-1.993-4.448-4.437.073-2.515 2.078-4.492 4.603-4.469l1.409-.01v1.996l-1.409.013h-.007c-1.388.04-2.577.984-2.6 2.47a2.438 2.438 0 0 0 2.452 2.439c1.356 0 2.441-.987 2.441-2.437l-.001-7.557c0-.14.005-.252.02-.407.23-1.848 1.883-3.256 3.754-3.256z"/></svg>

After

Width:  |  Height:  |  Size: 911 B

View file

@ -0,0 +1 @@
<svg fill="#54487A" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Gentoo</title><path d="M9.94 0a7.31 7.31 0 00-1.26.116c-4.344.795-7.4 4.555-7.661 7.031-.126 1.215.53 2.125.89 2.526.977 1.085 2.924 1.914 4.175 2.601-1.81 1.543-2.64 2.296-3.457 3.154C1.403 16.712.543 18.125.54 19.138c0 .325-.053 1.365.371 2.187.16.309.613 1.338 1.98 2.109.874.494 2.119.675 3.337.501 3.772-.538 8.823-3.737 12.427-6.716 2.297-1.9 3.977-3.739 4.462-4.644.39-.731.434-2.043.207-2.866-.645-2.337-5.887-7.125-10.172-9.051A7.824 7.824 0 009.94 0zm-.008.068a7.4 7.4 0 013.344.755c3.46 1.7 9.308 6.482 9.739 8.886.534 2.972-9.931 11.017-16.297 12.272-2.47.485-4.576.618-5.537-1.99-.832-2.262.783-3.916 3.16-6.09a92.546 92.546 0 012.96-2.576c.065-.069-5.706-2.059-5.89-4.343C1.221 4.634 4.938.3 9.697.076c.08-.004.157-.007.235-.008zm-.112.52a5.647 5.647 0 00-.506.032c-2.337.245-2.785.547-4.903 2.149-.71.537-2.016 1.844-2.35 3.393-.128.59.024 1.1.448 1.458 1.36 1.144 3.639 2.072 5.509 2.97.547.263.185.74-.698 1.505-2.227 1.928-5.24 4.276-5.45 6.066-.099.842.19 1.988 1.213 2.574 1.195.685 3.676.238 5.333-.379 2.422-.902 5.602-2.892 8.127-4.848 2.625-2.034 5.067-4.617 5.188-5.038.148-.517.133-.996-.154-1.546-.448-.862-1.049-1.503-1.694-2.22-1.732-1.825-3.563-3.43-5.754-4.658C12.694 1.242 11.417.564 9.82.588zm1.075 3.623c.546 0 1.176.173 1.853.5 1.688.817 3.422 2.961-.015 4.195-.935.336-3.9-.824-3.81-2.407.09-1.57.854-2.289 1.972-2.288zm.285 1.367c-.317-.002-.575.079-.694.263-.557.861-.303 1.472.212 1.862.192-.457 2.156.043 2.148.472a.32.32 0 00.055-.032c1.704-1.282-.472-2.557-1.72-2.565z"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1 @@
<svg fill="#0079C1" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Kubuntu</title><path d="M21.257 14.292a.2065.2065 0 01.1097.2374l-.5765 1.5293a.2063.2063 0 01-.2033.155l-2.3369-.2283c-.0628.001-.1623-.008-.2776.1255-.0178.0207-.7472.9328-.9783 1.1832-.079.0708-.1067.1652-.0878.281l.5521 2.1962a.2064.2064 0 01-.098.2314l-1.513.8906a.2063.2063 0 01-.2556-.0418l-1.5494-1.7055c-.0516-.0555-.1551-.0994-.2271-.0759l-1.645.2391c-.0804.011-.1267.0635-.1603.1164l-.9938 2.0793a.2063.2063 0 01-.2353.089l-1.6687-.3945a.2065.2065 0 01-.1462-.1824.209.209 0 01.0105-.0815l.8812-3.244a.222.222 0 01.2828-.1373l.0033-.001a5.8423 5.8423 0 002.7168.2176c2.3222-.3696 4.1964-2.0953 4.756-4.3791l.011-.0407c.0277-.1194.1768-.1827.2963-.155 0 0 2.8684.9737 3.2936 1.0816a.2089.2089 0 01.0394.0143zM5.539 4.9898l.0001.0001a.2051.2051 0 01.0659.0489l2.392 2.3567a.222.222 0 01-.0186.3138l-.0008.0034A5.8422 5.8422 0 006.4594 9.976c-.8132 2.2063-.2244 4.685 1.494 6.29l.0353.0396c.0906.0827.0678.2335-.0148.3241 0 0-2.2452 2.0243-2.5472 2.3425a.2064.2064 0 01-.2924.007l-1.0521-1.2507a.2063.2063 0 01-.0358-.253l1.335-1.9253c.0297-.0553.0863-.1376.0262-.3035-.0092-.0256-.4482-1.108-.5536-1.432-.0232-.1035-.092-.1738-.2022-.214l-2.1789-.594a.2064.2064 0 01-.154-.1986l-.0368-1.7552a.2065.2065 0 01.1615-.2026l2.2384-.516c.0737-.0177.1625-.0866.1772-.1609l.5958-1.5517c.0298-.0755.0068-.1417-.023-.1968L4.111 6.5396a.2064.2064 0 01.0374-.2488l1.1602-1.2626a.2066.2066 0 01.2305-.0385zm10.4906-1.747a.2139.2139 0 01.0313.0147l1.5385.8455a.2065.2065 0 01.0947.2412l-.6793 2.198c-.0214.0727-.0062.1841.0508.234l1.046 1.2918c.0505.0636.1193.0767.182.0785l2.3-.2029a.2064.2064 0 01.1968.1567l.5134 1.6361a.2064.2064 0 01-.082.2189h-.0001a.205.205 0 01-.0753.0326l-3.244.8946a.222.222 0 01-.2624-.1729.012.012 0 01-.0025-.0024 5.8422 5.8422 0 00-1.201-2.4466c-1.5041-1.8073-3.9452-2.5368-6.1943-1.851l-.0402.0123c-.1169.0371-.248-.0597-.285-.1766 0 0-.6236-2.958-.7481-3.3786a.2063.2063 0 01.14-.2568l1.6093-.2858a.2063.2063 0 01.237.0955l.9929 2.1203c.033.0534.076.1436.2498.1744.0268.0048 1.1835.1658 1.5169.2366.1012.0316.1966.0073.2864-.068l1.6107-1.5916a.2064.2064 0 01.2177-.0486zM16.021.6955A11.9968 11.9968 0 007.794.763C1.5889 3.086-1.5582 9.9993.7647 16.2044c2.323 6.205 9.2362 9.3522 15.4413 7.0293 6.2051-2.3229 9.3522-9.2362 7.0293-15.4413A11.997 11.997 0 0016.021.6955z"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

File diff suppressed because it is too large Load diff

After

Width:  |  Height:  |  Size: 475 KiB

View file

@ -0,0 +1 @@
<svg fill="#35BF5C" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Manjaro</title><path d="M0 0v24h6.75V6.75h8.625V0H0zm8.625 8.625V24h6.75V8.625h-6.75zM17.25 0v24H24V0h-6.75z"/></svg>

After

Width:  |  Height:  |  Size: 210 B

View file

@ -0,0 +1 @@
<svg fill="#87CF3E" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Linux Mint</title><path d="M0 1.693v4.193h1.828c1.276 0 1.502.865 1.502 2.058l.01 7.412c0 3.84 3.44 6.951 7.68 6.951h10.464c1.342 0 2.516-.83 2.516-2.108V8.706c0-3.84-3.44-6.95-7.683-6.95h-4.405v-.013L0 1.693zm5.723 2.566h2.102V14.82c0 1.413.984 2.51 2.139 2.51l7.17.03c1.496 0 2.661-1.01 2.661-2.206l-.012-5.607a1.2 1.2 0 0 0-.386-.91 1.224 1.224 0 0 0-.917-.384c-.374 0-.65.12-.918.384a1.2 1.2 0 0 0-.386.91v4.798h-2.223V9.548c0-.364-.124-.648-.389-.91a1.208 1.208 0 0 0-.917-.384c-.366 0-.647.12-.914.384-.265.262-.39.546-.39.91v4.798H10.12V9.548c0-.95.36-1.792 1.042-2.466a3.445 3.445 0 0 1 2.485-1.022c.937 0 1.752.345 2.413.97a3.448 3.448 0 0 1 2.42-.97c.954 0 1.803.348 2.485 1.022a3.385 3.385 0 0 1 1.041 2.466l.009 5.991c-.105 1.004-.539 1.894-1.28 2.637h-.002a4.367 4.367 0 0 1-3.174 1.314H9.574v-.038c-.976-.103-1.846-.519-2.57-1.217-.845-.825-1.281-1.846-1.281-3.01V4.26z"/></svg>

After

Width:  |  Height:  |  Size: 985 B

View file

@ -0,0 +1 @@
<svg fill="#73BA25" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>openSUSE</title><path d="M10.724 0a12 12 0 0 0-9.448 4.623c1.464.391 2.5.727 2.81.832.005-.19.037-1.893.037-1.893s.004-.04.025-.06c.026-.026.065-.018.065-.018.385.056 8.602 1.274 12.066 3.292.427.25.638.517.902.786.958.99 2.223 5.108 2.359 5.957.005.033-.036.07-.054.083a5.177 5.177 0 0 1-.313.228c-.82.55-2.708 1.872-5.13 1.656-2.176-.193-5.018-1.44-8.445-3.699.336.79.668 1.58 1 2.371.497.258 5.287 2.7 7.651 2.651 1.904-.04 3.941-.968 4.756-1.458 0 0 .179-.108.257-.048.085.066.061.167.041.27-.05.234-.164.66-.242.863l-.065.165c-.093.25-.183.482-.356.625-.48.436-1.246.784-2.446 1.305-1.855.812-4.865 1.328-7.66 1.31-1.001-.022-1.968-.133-2.817-.232-1.743-.197-3.161-.357-4.026.269A12 12 0 0 0 10.724 24a12 12 0 0 0 12-12 12 12 0 0 0-12-12zM13.4 6.963a3.503 3.503 0 0 0-2.521.942 3.498 3.498 0 0 0-1.114 2.449 3.528 3.528 0 0 0 3.39 3.64 3.48 3.48 0 0 0 2.524-.946 3.504 3.504 0 0 0 1.114-2.446 3.527 3.527 0 0 0-3.393-3.64zm-.03 1.035a2.458 2.458 0 0 1 2.368 2.539 2.43 2.43 0 0 1-.774 1.706 2.456 2.456 0 0 1-1.762.659 2.461 2.461 0 0 1-2.364-2.542c.02-.655.3-1.26.777-1.707a2.419 2.419 0 0 1 1.756-.655zm.402 1.23c-.602 0-1.087.325-1.087.727 0 .4.485.725 1.087.725.6 0 1.088-.326 1.088-.725 0-.402-.487-.726-1.088-.726Z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1 @@
<svg fill="#48B9C7" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Pop!_OS</title><path d="M15.724 11.253c-.776 1.366-.932 1.5-1.153 1.411-.261-.104-.171-1.472.067-3.134.067-.464.183-.684.312-.796.186-.158.524-.165.752-.131.263.038.514.16.704.344.168.163.187.33.13.546-.094.344-.256.674-.411.996-.036.079-.358.689-.401.764zm-1.41 2.034a.57.57 0 0 0-.382.411.53.53 0 0 0 .146.52.451.451 0 0 0 .543.055.484.484 0 0 0 .208-.309c.08-.336-.093-.794-.514-.677zm-5.01-1.239c.154.656.138 1.377.006 1.896-.123.49-.458.93-.989 1.076-.466.13-1.009.035-1.341-.285-.548-.525-.868-1.758-.725-2.653.115-.718.503-1.638 1.352-1.678a1.11 1.11 0 0 1 .163.003c.866.078 1.34.808 1.534 1.641zm-1.153.35a.807.807 0 0 0-.205-.333c-.344-.314-.688.013-.748.386-.046.291.021.63.203.865a.588.588 0 0 0 .336.226.32.32 0 0 0 .144-.006c.162-.046.255-.215.298-.378a1.296 1.296 0 0 0-.028-.76zm4.8.132c-.333.508-1.34.876-1.772.844v.066c-.002 1.212.12 2.14-.413 2.25-.432.087-.418-.292-.45-.61-.129-1.235-.246-3.285-.296-4.526.035-.533.392-.86.764-.94.573-.123 1.185-.088 1.692.226 1.003.62 1.083 1.76.475 2.69zm-1.042-1.266c-.077-.24-.315-.584-.602-.541-.062.01-.165.038-.194.099-.069.142-.064 1.047.056 1.425.02.062.062.122.125.144.227.083.432-.195.525-.363a.972.972 0 0 0 .09-.764zM5.52 9.69c.135.61.107 1.234-.081 1.635-.376.795-1.001 1.427-1.865 1.74.248.647.5 1.288.748 1.935.138.35.269.732.149 1.076-.125.352-.612.45-.965.075-.68-.724-2.98-5.308-3.15-5.652-.145-.297-.36-.614-.355-.957.011-.516.808-1.037 1.191-1.305.535-.373 1.153-.628 1.814-.63.516 0 .956.142 1.347.392A2.698 2.698 0 0 1 5.52 9.69zm-1.833.763A2.533 2.533 0 0 0 2.864 9.2a.883.883 0 0 0-.277-.168c-.717-.235-.407.964-.29 1.266.162.424.407.96.735 1.28.072.07.157.134.255.153.149.03.287-.074.362-.195a.845.845 0 0 0 .1-.338 1.999 1.999 0 0 0-.063-.745zm18.128 5.574zm1.046-1.318c-.355-.05-.556-.117-.556-.355 0-.243.31-.379.6-.383.133-.003.308.02.478.101l.027.355h.421c0-.216.003-.344.003-.56-.328-.263-.586-.32-.949-.312-.45 0-1.058.267-1.058.804 0 .602.432.733.97.788.314.032.705.093.716.391 0 .328-.35.413-.687.413a1.21 1.21 0 0 1-.544-.142l-.03-.367h-.435c.003.076.001.51 0 .586.328.286.587.363 1.002.362.544 0 1.178-.178 1.182-.853-.002-.62-.561-.75-1.14-.828zm-1.045 1.318c0 .017 0 .015 0 0zm-.398-1.046c-.003.703-.432 1.406-1.378 1.406s-1.386-.692-1.386-1.398c0-.88.613-1.425 1.406-1.425.918.008 1.366.69 1.358 1.417zm-.478 0c.008-.475-.243-.962-.88-.973-.595 0-.928.413-.93.977 0 .49.273.95.918.95s.892-.499.892-.955zm-3.096.934H15.27a.223.223 0 0 0-.223.223v.02c0 .122.099.222.223.222h2.573a.223.223 0 0 0 .223-.223v-.019a.222.222 0 0 0-.223-.223z"/></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1 @@
<svg fill="#EE0000" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Red Hat</title><path d="M16.009 13.386c1.577 0 3.86-.326 3.86-2.202a1.765 1.765 0 0 0-.04-.431l-.94-4.08c-.216-.898-.406-1.305-1.982-2.093-1.223-.625-3.888-1.658-4.676-1.658-.733 0-.947.946-1.822.946-.842 0-1.467-.706-2.255-.706-.757 0-1.25.515-1.63 1.576 0 0-1.06 2.99-1.197 3.424a.81.81 0 0 0-.028.245c0 1.162 4.577 4.974 10.71 4.974m4.101-1.435c.218 1.032.218 1.14.218 1.277 0 1.765-1.984 2.745-4.593 2.745-5.895.004-11.06-3.451-11.06-5.734a2.326 2.326 0 0 1 .19-.925C2.746 9.415 0 9.794 0 12.217c0 3.969 9.405 8.861 16.851 8.861 5.71 0 7.149-2.582 7.149-4.62 0-1.605-1.387-3.425-3.887-4.512"/></svg>

After

Width:  |  Height:  |  Size: 696 B

View file

@ -0,0 +1 @@
<svg fill="#10B981" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Rocky Linux</title><path d="M23.332 15.957c.433-1.239.668-2.57.668-3.957 0-6.627-5.373-12-12-12S0 5.373 0 12c0 3.28 1.315 6.251 3.447 8.417L15.62 8.245l3.005 3.005zm-2.192 3.819l-5.52-5.52L6.975 22.9c1.528.706 3.23 1.1 5.025 1.1 3.661 0 6.94-1.64 9.14-4.224z"/></svg>

After

Width:  |  Height:  |  Size: 360 B

View file

@ -0,0 +1 @@
<svg fill="#E95420" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Ubuntu</title><path d="M17.61.455a3.41 3.41 0 0 0-3.41 3.41 3.41 3.41 0 0 0 3.41 3.41 3.41 3.41 0 0 0 3.41-3.41 3.41 3.41 0 0 0-3.41-3.41zM12.92.8C8.923.777 5.137 2.941 3.148 6.451a4.5 4.5 0 0 1 .26-.007 4.92 4.92 0 0 1 2.585.737A8.316 8.316 0 0 1 12.688 3.6 4.944 4.944 0 0 1 13.723.834 11.008 11.008 0 0 0 12.92.8zm9.226 4.994a4.915 4.915 0 0 1-1.918 2.246 8.36 8.36 0 0 1-.273 8.303 4.89 4.89 0 0 1 1.632 2.54 11.156 11.156 0 0 0 .559-13.089zM3.41 7.932A3.41 3.41 0 0 0 0 11.342a3.41 3.41 0 0 0 3.41 3.409 3.41 3.41 0 0 0 3.41-3.41 3.41 3.41 0 0 0-3.41-3.41zm2.027 7.866a4.908 4.908 0 0 1-2.915.358 11.1 11.1 0 0 0 7.991 6.698 11.234 11.234 0 0 0 2.422.249 4.879 4.879 0 0 1-.999-2.85 8.484 8.484 0 0 1-.836-.136 8.304 8.304 0 0 1-5.663-4.32zm11.405.928a3.41 3.41 0 0 0-3.41 3.41 3.41 3.41 0 0 0 3.41 3.41 3.41 3.41 0 0 0 3.41-3.41 3.41 3.41 0 0 0-3.41-3.41z"/></svg>

After

Width:  |  Height:  |  Size: 963 B

View file

@ -0,0 +1 @@
<svg fill="#0078D4" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Windows</title><path d="M0,0H11.377V11.372H0ZM12.623,0H24V11.372H12.623ZM0,12.623H11.377V24H0Zm12.623,0H24V24H12.623"/></svg>

After

Width:  |  Height:  |  Size: 218 B

View file

@ -64,10 +64,12 @@ developerMode=Developer mode
developerModeDescription=When enabled, you will have access to a variety of additional options that are useful for development.
editor=Editor
custom=Custom
passwordManagerCommand=Password manager command
passwordManagerCommandDescription=The command to execute to fetch passwords. The placeholder string $KEY will be replaced by the quoted password key when called. This should call your password manager CLI to print the password to stdout, e.g. mypassmgr get $KEY.\n\nYou can check here whether the output is correct. The command should only output the password itself, no other formatting should be included in the output.
preferEditorTabs=Prefer to open new tabs
preferEditorTabsDescription=Controls whether XPipe will try to open new tabs in your chosen editor instead of new windows.
customEditorCommand=Custom editor command
customEditorCommandDescription=The command to execute to open the custom editor. The placeholder string $file will be replaced by the quoted absolute file name when called. Remember to quote your editor executable path if it contains spaces.
customEditorCommandDescription=The command to execute to open the custom editor. The placeholder string $FILE will be replaced by the quoted absolute file name when called. Remember to quote your editor executable path if it contains spaces.
editorReloadTimeout=Editor reload timeout
editorReloadTimeoutDescription=The amount of milliseconds to wait before reading a file after it has been updated. This avoids issues in cases where your editor is slow at writing or releasing file locks.
notepad++=Notepad++
@ -102,7 +104,7 @@ terminalProgram=Default program
terminalProgramDescription=The default terminal to use when opening any kind of shell connection. This application is only used for display purposes, the started shell program depends on the shell connection itself.
program=Program
customTerminalCommand=Custom terminal command
customTerminalCommandDescription=The command to execute to open the custom terminal. The placeholder string $cmd will be replaced by the quoted shell script file name when called. Remember to quote your terminal executable path if it contains spaces.
customTerminalCommandDescription=The command to execute to open the custom terminal. The placeholder string $CMD will be replaced by the quoted shell script file name when called. Remember to quote your terminal executable path if it contains spaces.
preferTerminalTabs=Prefer to open new tabs
preferTerminalTabsDescription=Controls whether XPipe will try to open new tabs in your chosen terminal instead of new windows.
cmd=cmd.exe

View file

@ -4,9 +4,14 @@ crlf=CRLF (Windows)
lf=LF (Linux)
none=None
common=Common
key=Key
passwordManager=Password manager
prompt=Prompt
customCommand=Custom command
other=Other
setLock=Set lock
changeLock=Change lock
test=Test
lockCreationAlertTitle=Create Lock
lockCreationAlertHeader=Set your new lock password
finish=Finish
@ -199,6 +204,7 @@ downloadUpdate=Download update
legalAccept=I accept the EULA and the privacy policy
confirm=Confirm
whatsNew=What's new in version $VERSION$?
antivirusNoticeTitle=A note on Antivirus programs
updateChangelogAlertTitle=Changelog
greetingsAlertTitle=Welcome to XPipe
gotIt=Got It

View file

@ -0,0 +1,16 @@
### Information about antivirus programs
XPipe detected that you are running %s. As a result, you might run into a few problems due to %s detecting suspicious activity when XPipe is interacting with your shell programs. This can range from just notifications to full isolation of XPipe and your shell programs, effectively making the application unusable.
### Threat analysis
For this reason, all artifacts of every release are automatically uploaded and analyzed on [VirusTotal](https://virustotal.com), so uploading the release you downloaded to VirusTotal should instantly show complete analysis results. From there you should be able to get a more accurate overview over the threat level of XPipe to you.
You can find the analysis results listed at the bottom of every release on GitHub, i.e. here:
[https://github.com/xpipe-io/xpipe/releases/tag/%s](https://github.com/xpipe-io/xpipe/releases/tag/%s)
### What you can do
If such a false-positive also happens on your end, you might have to explicitly whitelist XPipe in order for it to work correctly. Accessing shells is necessary for XPipe, there is no fallback alternative built in that does not launch shells.
If you choose to continue from here, XPipe will start calling shell programs to properly initialize, which might provoke %s.

View file

@ -1,8 +1,8 @@
.prefs * {
-fx-text-fill: -color-fg-default;
-fx-highlight-text-fill: -color-fg-default;
-fx-highlight-text-fill: -color-fg-default;
-fx-highlight-fill: -color-neutral-muted;
-fx-prompt-text-fill: -color-neutral-muted;
-fx-prompt-text-fill: -color-fg-subtle;
-fx-text-box-border:-color-neutral-muted;
-fx-control-inner-background: -color-neutral-muted;
-fx-body-color: -color-neutral-muted;

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more