Compare commits

...

35 commits

Author SHA1 Message Date
crschnick
4c7f91fec9 Rework file transfers 2024-05-06 03:04:30 +00:00
crschnick
8d6eb1051c Refactor file transfers 2024-05-06 02:11:46 +00:00
crschnick
33e75fec2a Rework file icon loading 2024-05-06 00:45:43 +00:00
crschnick
e4f5738fa5 Rework error actions 2024-05-06 00:14:14 +00:00
crschnick
1cabd5b93d Check for desktop support 2024-05-05 02:28:53 +00:00
crschnick
cb9145dd37 [stage] 2024-05-04 21:30:27 +00:00
crschnick
ab13f17fe7 Platform sync fixes 2024-05-04 21:28:37 +00:00
crschnick
8051412e9f [release] 2024-05-03 16:36:17 +00:00
crschnick
650398b541 Reformat 2024-05-03 16:17:57 +00:00
crschnick
2d367aadb9 Rebase 2024-05-03 16:12:57 +00:00
Christopher Schnick
7d0db085c0
Merge pull request #245 from tacaly/master
DA lang context fix "hibernation option"
2024-05-03 18:10:28 +02:00
Frederick Ambo
db4dc20c70
Merge branch 'xpipe-io:master' into master 2024-05-03 18:06:21 +02:00
crschnick
48e9ece152 Various fixes 2024-05-03 15:46:47 +00:00
Frederick Ambo
12a2e85535 DA lang context "hibernation option"
Added the correct text context. It will prob. be changed with time, but for now it's WAY better than "hvælving" = Arch.
2024-05-03 02:35:12 +02:00
crschnick
fbf9902b58 Remove bug report button 2024-05-02 23:39:52 +00:00
crschnick
5d6c8b95bc Fix property characters 2024-05-02 23:18:41 +00:00
crschnick
dcbbd211fe Build fixes 2024-05-02 23:12:37 +00:00
crschnick
60207dd24f Small fixes [stage] 2024-05-02 20:44:34 +00:00
crschnick
3bc1dc6cad Small fixes 2024-05-02 20:19:14 +00:00
crschnick
366a6e74e7 Add lock on hibernation option 2024-05-02 16:04:24 +00:00
crschnick
9ad98e4638 Fix desktop environment issues with null 2024-05-02 15:13:13 +00:00
crschnick
e5fdec3a5a [stage] 2024-05-01 19:12:36 +00:00
crschnick
e5c2079264 Fix NPEs 2024-05-01 19:00:00 +00:00
crschnick
568d1c2e6f Fix popover double show 2024-05-01 17:33:21 +00:00
crschnick
fd629c62bc Merge branch 'package-ptb' 2024-05-01 17:33:11 +00:00
crschnick
6408390535 Reformat 2024-05-01 17:16:19 +00:00
crschnick
c7f6bcf7d7 VNC improvements 2024-05-01 17:01:36 +00:00
crschnick
7176f4dd0a [stage] 2024-04-29 23:06:37 +00:00
crschnick
6fc48a7d74 Rework macos app launches 2024-04-29 22:49:30 +00:00
crschnick
dc50b0b155 Prepare next version 2024-04-29 20:23:19 +00:00
crschnick
733df4c005 Rework elevation function 2024-04-29 19:38:22 +00:00
crschnick
9c3eaa479c Various fixes 2024-04-28 21:09:40 +00:00
crschnick
bed38d425f Fix build [release] [noannounce] 2024-04-27 18:41:07 +00:00
crschnick
3d0dd67389 [release] [noannounce] 2024-04-27 18:21:45 +00:00
crschnick
b112d23163 Fix NPE with tabby 2024-04-27 18:12:52 +00:00
141 changed files with 1431 additions and 883 deletions

View file

@ -43,7 +43,6 @@ dependencies {
api 'info.picocli:picocli:4.7.5'
api 'org.kohsuke:github-api:1.321'
api 'io.sentry:sentry:7.8.0'
api 'org.ocpsoft.prettytime:prettytime:5.0.7.Final'
api 'commons-io:commons-io:2.16.1'
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.17.0"
api group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: "2.17.0"
@ -75,7 +74,6 @@ application {
mainModule = 'io.xpipe.app'
mainClass = 'io.xpipe.app.Main'
applicationDefaultJvmArgs = jvmRunArgs
applicationDefaultJvmArgs.add('-XX:+EnableDynamicAgentLoading')
}
run {

View file

@ -86,8 +86,7 @@ public final class BrowserBookmarkComp extends SimpleComp {
selectedCategory)
.styleClass(Styles.LEFT_PILL)
.minWidth(Region.USE_PREF_SIZE);
var filter =
new FilterComp(filterText).hgrow();
var filter = new FilterComp(filterText).hgrow();
var top = new HorizontalComp(List.of(category, filter))
.styleClass("categories")

View file

@ -1,17 +1,16 @@
package io.xpipe.app.browser;
import io.xpipe.app.browser.file.FileSystemHelper;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.LocalFileSystem;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.ProcessControlProvider;
import io.xpipe.core.store.FileSystem;
import io.xpipe.core.util.FailableRunnable;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.Dragboard;
import lombok.SneakyThrows;
import lombok.Value;
@ -45,14 +44,14 @@ public class BrowserClipboard {
List<File> data = (List<File>) clipboard.getData(DataFlavor.javaFileListFlavor);
var files =
data.stream().map(string -> string.toPath()).toList();
data.stream().map(f -> f.toPath()).toList();
if (files.size() == 0) {
return;
}
var entries = new ArrayList<FileSystem.FileEntry>();
var entries = new ArrayList<BrowserEntry>();
for (Path file : files) {
entries.add(FileSystemHelper.getLocal(file));
entries.add(LocalFileSystem.getLocalBrowserEntry(file));
}
currentCopyClipboard.setValue(new Instance(UUID.randomUUID(), null, entries));
@ -64,7 +63,7 @@ public class BrowserClipboard {
}
@SneakyThrows
public static ClipboardContent startDrag(FileSystem.FileEntry base, List<FileSystem.FileEntry> selected) {
public static ClipboardContent startDrag(FileSystem.FileEntry base, List<BrowserEntry> selected) {
if (selected.isEmpty()) {
return null;
}
@ -77,7 +76,7 @@ public class BrowserClipboard {
}
@SneakyThrows
public static void startCopy(FileSystem.FileEntry base, List<FileSystem.FileEntry> selected) {
public static void startCopy(FileSystem.FileEntry base, List<BrowserEntry> selected) {
if (selected.isEmpty()) {
currentCopyClipboard.setValue(null);
return;
@ -118,11 +117,11 @@ public class BrowserClipboard {
public static class Instance {
UUID uuid;
FileSystem.FileEntry baseDirectory;
List<FileSystem.FileEntry> entries;
List<BrowserEntry> entries;
public String toClipboardString() {
return entries.stream()
.map(fileEntry -> "\"" + fileEntry.getPath() + "\"")
.map(fileEntry -> "\"" + fileEntry.getRawFileEntry().getPath() + "\"")
.collect(Collectors.joining(ProcessControlProvider.get()
.getEffectiveLocalDialect()
.getNewLine()

View file

@ -1,6 +1,6 @@
package io.xpipe.app.browser;
import io.xpipe.app.browser.icon.FileIconManager;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.core.AppStyle;
import io.xpipe.app.core.AppWindowHelper;
@ -8,9 +8,6 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
@ -21,7 +18,6 @@ import javafx.scene.control.OverrunStyle;
import javafx.scene.image.Image;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Value;
@ -33,14 +29,14 @@ import java.util.function.Function;
@AllArgsConstructor
public class BrowserSelectionListComp extends SimpleComp {
ObservableList<FileSystem.FileEntry> list;
Function<FileSystem.FileEntry, ObservableValue<String>> nameTransformation;
ObservableList<BrowserEntry> list;
Function<BrowserEntry, ObservableValue<String>> nameTransformation;
public BrowserSelectionListComp(ObservableList<FileSystem.FileEntry> list) {
this(list, entry -> new SimpleStringProperty(FileNames.getFileName(entry.getPath())));
public BrowserSelectionListComp(ObservableList<BrowserEntry> list) {
this(list, entry -> new SimpleStringProperty(entry.getFileName()));
}
public static Image snapshot(ObservableList<FileSystem.FileEntry> list) {
public static Image snapshot(ObservableList<BrowserEntry> list) {
var r = new BrowserSelectionListComp(list).styleClass("drag").createRegion();
var scene = new Scene(r);
AppWindowHelper.setupStylesheets(scene);
@ -54,7 +50,7 @@ public class BrowserSelectionListComp extends SimpleComp {
protected Region createSimple() {
var c = new ListBoxViewComp<>(list, list, entry -> {
return Comp.of(() -> {
var image = PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(entry, false), 24)
var image = PrettyImageHelper.ofFixedSizeSquare(entry.getIcon(), 24)
.createRegion();
var l = new Label(null, image);
l.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);

View file

@ -1,5 +1,6 @@
package io.xpipe.app.browser;
import atlantafx.base.controls.Spacer;
import io.xpipe.app.browser.file.BrowserContextMenu;
import io.xpipe.app.browser.file.BrowserFileListCompEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
@ -11,16 +12,16 @@ import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.impl.LabelComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.util.HumanReadableFormat;
import javafx.beans.binding.Bindings;
import javafx.scene.control.ToolBar;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region;
import atlantafx.base.controls.Spacer;
import lombok.EqualsAndHashCode;
import lombok.Value;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
@Value
@EqualsAndHashCode(callSuper = true)
public class BrowserStatusBarComp extends SimpleComp {
@ -56,7 +57,9 @@ public class BrowserStatusBarComp extends SimpleComp {
var transferred = HumanReadableFormat.progressByteCount(p.getTransferred());
var all = HumanReadableFormat.byteCount(p.getTotal());
var name = (p.getName() != null ? " @ " + p.getName() + " " : "");
return transferred + " / " + all + name;
var time = p.getTotal() > 50_000_000 && p.elapsedTime().compareTo(Duration.of(200, ChronoUnit.MILLIS)) > 0 ? " | "
+ HumanReadableFormat.duration(p.expectedTimeRemaining()) : " | ...";
return transferred + " / " + all + name + time;
}
});
var progressComp = new LabelComp(text).styleClass("progress");
@ -87,9 +90,7 @@ public class BrowserStatusBarComp extends SimpleComp {
var allCount = Bindings.createIntegerBinding(
() -> {
return (int) model.getFileList().getAll().getValue().stream()
.filter(entry -> !entry.isSynthetic())
.count();
return model.getFileList().getAll().getValue().size();
},
model.getFileList().getAll());
var selectedComp = new LabelComp(Bindings.createStringBinding(

View file

@ -9,10 +9,7 @@ import io.xpipe.app.fxcomps.augment.DragOverPseudoClassAugment;
import io.xpipe.app.fxcomps.impl.*;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileNames;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
@ -21,7 +18,6 @@ import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Region;
import org.kordamp.ikonli.javafx.FontIcon;
import java.io.File;
@ -50,25 +46,21 @@ public class BrowserTransferComp extends SimpleComp {
var backgroundStack =
new StackComp(List.of(background)).grow(true, true).styleClass("download-background");
var binding = ListBindingsHelper.mappedContentBinding(syncItems, item -> item.getFileEntry());
var binding = ListBindingsHelper.mappedContentBinding(syncItems, item -> item.getBrowserEntry());
var list = new BrowserSelectionListComp(
binding,
entry -> Bindings.createStringBinding(
() -> {
var sourceItem = syncItems.stream()
.filter(item -> item.getFileEntry() == entry)
.filter(item -> item.getBrowserEntry() == entry)
.findAny();
if (sourceItem.isEmpty()) {
return "?";
}
var name =
sourceItem.get().downloadFinished().get()
var name = entry.getModel() == null || sourceItem.get().downloadFinished().get()
? "Local"
: DataStorage.get()
.getStoreDisplayName(entry.getFileSystem()
.getStore())
.orElse("?");
return FileNames.getFileName(entry.getPath()) + " (" + name + ")";
: entry.getModel().getFileSystemModel().getName();
return entry.getFileName() + " (" + name + ")";
},
syncAllDownloaded))
.apply(struc -> struc.get().setMinHeight(150))
@ -154,7 +146,7 @@ public class BrowserTransferComp extends SimpleComp {
}
var selected = syncItems.stream()
.map(BrowserTransferModel.Item::getFileEntry)
.map(item -> item.getBrowserEntry())
.toList();
Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);

View file

@ -1,14 +1,13 @@
package io.xpipe.app.browser;
import io.xpipe.app.browser.file.FileSystemHelper;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileTransferOperation;
import io.xpipe.app.browser.file.LocalFileSystem;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ShellTemp;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
@ -17,7 +16,6 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Value;
import org.apache.commons.io.FileUtils;
@ -66,9 +64,9 @@ public class BrowserTransferModel {
items.clear();
}
public void drop(OpenFileSystemModel model, List<FileSystem.FileEntry> entries) {
public void drop(OpenFileSystemModel model, List<BrowserEntry> entries) {
entries.forEach(entry -> {
var name = FileNames.getFileName(entry.getPath());
var name = entry.getFileName();
if (items.stream().anyMatch(item -> item.getName().equals(name))) {
return;
}
@ -89,14 +87,14 @@ public class BrowserTransferModel {
try {
var paths = entries.stream().map(File::toPath).filter(Files::exists).toList();
for (Path path : paths) {
var entry = FileSystemHelper.getLocal(path);
var name = entry.getName();
var entry = LocalFileSystem.getLocalBrowserEntry(path);
var name = entry.getFileName();
if (items.stream().anyMatch(item -> item.getName().equals(name))) {
return;
}
var item = new Item(null, name, entry, path);
item.progress.setValue(BrowserTransferProgress.finished(entry.getName(), entry.getSize()));
item.progress.setValue(BrowserTransferProgress.finished(entry.getFileName(), entry.getRawFileEntry().getSize()));
items.add(item);
}
} catch (Exception ex) {
@ -128,15 +126,16 @@ public class BrowserTransferModel {
try {
try (var b = new BooleanScope(downloading).start()) {
FileSystemHelper.dropFilesInto(
FileSystemHelper.getLocal(TEMP),
List.of(item.getFileEntry()),
var op = new BrowserFileTransferOperation(
LocalFileSystem.getLocalFileEntry(TEMP),
List.of(item.getBrowserEntry().getRawFileEntry()),
true,
false,
progress -> {
item.getProgress().setValue(progress);
item.getOpenFileSystemModel().getProgress().setValue(progress);
});
op.execute();
}
} catch (Throwable t) {
ErrorEvent.fromThrowable(t).handle();
@ -151,15 +150,15 @@ public class BrowserTransferModel {
public static class Item {
OpenFileSystemModel openFileSystemModel;
String name;
FileSystem.FileEntry fileEntry;
BrowserEntry browserEntry;
Path localFile;
Property<BrowserTransferProgress> progress;
public Item(
OpenFileSystemModel openFileSystemModel, String name, FileSystem.FileEntry fileEntry, Path localFile) {
OpenFileSystemModel openFileSystemModel, String name, BrowserEntry browserEntry, Path localFile) {
this.openFileSystemModel = openFileSystemModel;
this.name = name;
this.fileEntry = fileEntry;
this.browserEntry = browserEntry;
this.localFile = localFile;
this.progress = new SimpleObjectProperty<>();
}

View file

@ -2,26 +2,45 @@ package io.xpipe.app.browser;
import lombok.Value;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
@Value
public class BrowserTransferProgress {
String name;
long transferred;
long total;
Instant start;
public static BrowserTransferProgress empty() {
return new BrowserTransferProgress(null, 0, 0);
return new BrowserTransferProgress(null, 0, 0, Instant.now());
}
static BrowserTransferProgress empty(String name, long size) {
return new BrowserTransferProgress(name, 0, size);
return new BrowserTransferProgress(name, 0, size, Instant.now());
}
public static BrowserTransferProgress finished(String name, long size) {
return new BrowserTransferProgress(name, size, size);
return new BrowserTransferProgress(name, size, size, Instant.now());
}
public boolean done() {
return transferred >= total;
}
public Duration elapsedTime() {
var now = Instant.now();
var elapsed = Duration.between(start,now);
return elapsed;
}
public Duration expectedTimeRemaining() {
var elapsed = elapsedTime();
var share = (double) transferred / total;
var rest = (1.0 - share) / share;
var restMillis = (long) (elapsed.toMillis() * rest);
return Duration.of(restMillis, ChronoUnit.MILLIS);
}
}

View file

@ -1,6 +1,5 @@
package io.xpipe.app.browser;
import atlantafx.base.theme.Styles;
import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.ListBoxViewComp;
@ -32,6 +31,7 @@ import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import java.util.List;
@ -139,14 +139,18 @@ public class BrowserWelcomeComp extends SimpleComp {
private Comp<?> entryButton(BrowserSavedState.Entry e, BooleanProperty disable) {
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
var graphic = entry.get().getProvider().getDisplayIconFileName(entry.get().getStore());
var graphic =
entry.get().getProvider().getDisplayIconFileName(entry.get().getStore());
var view = PrettyImageHelper.ofFixedSize(graphic, 30, 24);
return new ButtonComp(
new SimpleStringProperty(DataStorage.get().getStoreDisplayName(entry.get())),
view.createRegion(),
() -> {
ThreadHelper.runAsync(() -> {
model.restoreStateAsync(e, disable);
var storageEntry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
if (storageEntry.isPresent()) {
model.openFileSystemAsync(storageEntry.get().ref(), null, disable);
}
});
})
.minWidth(250)

View file

@ -8,8 +8,10 @@ import io.xpipe.app.fxcomps.util.Shortcuts;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.LicenseProvider;
import io.xpipe.app.util.ThreadHelper;
import javafx.scene.control.Button;
import javafx.scene.control.MenuItem;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
@ -91,7 +93,8 @@ public interface LeafAction extends BrowserAction {
mi.setMnemonicParsing(false);
mi.setDisable(!isActive(model, selected));
if (getProFeatureId() != null && !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) {
if (getProFeatureId() != null
&& !LicenseProvider.get().getFeature(getProFeatureId()).isSupported()) {
mi.setDisable(true);
}

View file

@ -32,8 +32,7 @@ public final class BrowserContextMenu extends ContextMenu {
? selected.stream()
.map(browserEntry -> new BrowserEntry(
browserEntry.getRawFileEntry().resolved(),
browserEntry.getModel(),
browserEntry.isSynthetic()))
browserEntry.getModel()))
.toList()
: selected;
}
@ -44,7 +43,7 @@ public final class BrowserContextMenu extends ContextMenu {
var empty = source == null;
var selected = new ArrayList<>(
empty
? List.of(new BrowserEntry(model.getCurrentDirectory(), model.getFileList(), false))
? List.of(new BrowserEntry(model.getCurrentDirectory(), model.getFileList()))
: model.getFileList().getSelection());
if (source != null && !selected.contains(source)) {
selected.add(source);

View file

@ -13,14 +13,12 @@ public class BrowserEntry {
private final BrowserFileListModel model;
private final FileSystem.FileEntry rawFileEntry;
private final boolean synthetic;
private final BrowserIconFileType fileType;
private final BrowserIconDirectoryType directoryType;
public BrowserEntry(FileSystem.FileEntry rawFileEntry, BrowserFileListModel model, boolean synthetic) {
public BrowserEntry(FileSystem.FileEntry rawFileEntry, BrowserFileListModel model) {
this.rawFileEntry = rawFileEntry;
this.model = model;
this.synthetic = synthetic;
this.fileType = fileType(rawFileEntry);
this.directoryType = directoryType(rawFileEntry);
}
@ -52,6 +50,17 @@ public class BrowserEntry {
return null;
}
public String getIcon() {
if (fileType != null) {
return fileType.getIcon();
} else if (directoryType != null) {
return directoryType.getIcon(rawFileEntry, false);
} else {
return rawFileEntry.getKind() == FileKind.DIRECTORY
? "default_folder.svg"
: "default_file.svg";
}
}
public String getFileName() {
return getRawFileEntry().getName();
@ -61,9 +70,4 @@ public class BrowserEntry {
var n = getFileName();
return FileNames.quoteIfNecessary(n);
}
public String getOptionallyQuotedFilePath() {
var n = rawFileEntry.getPath();
return FileNames.quoteIfNecessary(n);
}
}

View file

@ -1,7 +1,8 @@
package io.xpipe.app.browser.file;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.icon.FileIconManager;
import io.xpipe.app.comp.base.LazyTextFieldComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp;
@ -16,7 +17,6 @@ import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
@ -38,9 +38,6 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList;
@ -137,29 +134,7 @@ public final class BrowserFileListComp extends SimpleComp {
table.getSelectionModel().setCellSelectionEnabled(false);
table.getSelectionModel().getSelectedItems().addListener((ListChangeListener<? super BrowserEntry>) c -> {
var toSelect = new ArrayList<>(c.getList());
// Explicitly unselect synthetic entries since we can't use a custom selection model as that is bugged in
// JavaFX
toSelect.removeIf(entry -> fileList.getFileSystemModel().getCurrentParentDirectory() != null
&& entry.getRawFileEntry()
.getPath()
.equals(fileList.getFileSystemModel()
.getCurrentParentDirectory()
.getPath()));
// Remove unsuitable selection
toSelect.removeIf(browserEntry -> (browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY
&& !fileList.getSelectionMode().isAcceptsDirectories())
|| (browserEntry.getRawFileEntry().getKind() != FileKind.DIRECTORY
&& !fileList.getSelectionMode().isAcceptsFiles()));
fileList.getSelection().setAll(toSelect);
Platform.runLater(() -> {
var toUnselect = table.getSelectionModel().getSelectedItems().stream()
.filter(entry -> !toSelect.contains(entry))
.toList();
toUnselect.forEach(entry -> table.getSelectionModel()
.clearSelection(table.getItems().indexOf(entry)));
});
fileList.getSelection().setAll(c.getList());
});
fileList.getSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {
@ -174,7 +149,6 @@ public final class BrowserFileListComp extends SimpleComp {
}
var indices = c.getList().stream()
.skip(1)
.mapToInt(entry -> table.getItems().indexOf(entry))
.toArray();
table.getSelectionModel()
@ -276,10 +250,6 @@ public final class BrowserFileListComp extends SimpleComp {
},
null,
() -> {
if (row.getItem() != null && row.getItem().isSynthetic()) {
return null;
}
return new BrowserContextMenu(fileList.getFileSystemModel(), row.getItem());
})
.augment(new SimpleCompStructure<>(row));
@ -573,15 +543,7 @@ public final class BrowserFileListComp extends SimpleComp {
// Visibility seems to be bugged, so use opacity
setOpacity(0.0);
} else {
var isParentLink = getTableRow()
.getItem()
.getRawFileEntry()
.equals(fileList.getFileSystemModel().getCurrentParentDirectory());
img.set(FileIconManager.getFileIcon(
isParentLink
? fileList.getFileSystemModel().getCurrentDirectory()
: getTableRow().getItem().getRawFileEntry(),
isParentLink));
img.set(getTableRow().getItem().getIcon());
var isDirectory = getTableRow().getItem().getRawFileEntry().getKind() == FileKind.DIRECTORY;
pseudoClassStateChanged(FOLDER, isDirectory);
@ -594,9 +556,8 @@ public final class BrowserFileListComp extends SimpleComp {
.resolved()
.getPath()
: getTableRow().getItem().getFileName();
var fileName = isParentLink ? ".." : normalName;
var hidden = !isParentLink
&& (getTableRow().getItem().getRawFileEntry().isHidden() || fileName.startsWith("."));
var fileName = normalName;
var hidden = getTableRow().getItem().getRawFileEntry().isHidden() || fileName.startsWith(".");
getTableRow().pseudoClassStateChanged(HIDDEN, hidden);
text.set(fileName);
// Visibility seems to be bugged, so use opacity

View file

@ -109,7 +109,7 @@ public class BrowserFileListCompEntry {
if (!Objects.equals(
model.getFileSystemModel().getFileSystem(),
cb.getEntries().getFirst().getFileSystem())) {
cb.getEntries().getFirst().getRawFileEntry().getFileSystem())) {
return true;
}
@ -157,7 +157,7 @@ public class BrowserFileListCompEntry {
var target = item != null && item.getRawFileEntry().getKind() == FileKind.DIRECTORY
? item.getRawFileEntry()
: model.getFileSystemModel().getCurrentDirectory();
model.getFileSystemModel().dropFilesIntoAsync(target, files, false);
model.getFileSystemModel().dropFilesIntoAsync(target, files.stream().map(browserEntry -> browserEntry.getRawFileEntry()).toList(), false);
event.setDropCompleted(true);
event.consume();
}
@ -182,7 +182,7 @@ public class BrowserFileListCompEntry {
return;
}
var selected = model.getSelectedRaw();
var selected = model.getSelection();
Dragboard db = row.startDragAndDrop(TransferMode.COPY);
db.setContent(BrowserClipboard.startDrag(model.getFileSystemModel().getCurrentDirectory(), selected));
@ -244,7 +244,7 @@ public class BrowserFileListCompEntry {
return;
}
if (item == null || item.isSynthetic()) {
if (item == null) {
return;
}

View file

@ -59,10 +59,7 @@ public final class BrowserFileListModel {
public void setAll(Stream<FileSystem.FileEntry> newFiles) {
try (var s = newFiles) {
var parent = fileSystemModel.getCurrentParentDirectory();
var l = Stream.concat(
parent != null ? Stream.of(new BrowserEntry(parent, this, true)) : Stream.of(),
s.filter(entry -> entry != null).map(entry -> new BrowserEntry(entry, this, false)))
var l = s.filter(entry -> entry != null).map(entry -> new BrowserEntry(entry, this))
.toList();
all.setValue(l);
refreshShown();
@ -94,14 +91,13 @@ public final class BrowserFileListModel {
}
public Comparator<BrowserEntry> order() {
var syntheticFirst = Comparator.<BrowserEntry, Boolean>comparing(path -> !path.isSynthetic());
var dirsFirst = Comparator.<BrowserEntry, Boolean>comparing(
path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);
var comp = comparatorProperty.getValue();
Comparator<BrowserEntry> us = comp != null
? syntheticFirst.thenComparing(dirsFirst).thenComparing(comp)
: syntheticFirst.thenComparing(dirsFirst);
? dirsFirst.thenComparing(comp)
: dirsFirst;
return us;
}

View file

@ -37,7 +37,7 @@ public class BrowserFileOverviewComp extends SimpleComp {
var graphic = new HorizontalComp(List.of(
icon,
new BrowserQuickAccessButtonComp(
() -> new BrowserEntry(entry, model.getFileList(), false), model)));
() -> new BrowserEntry(entry, model.getFileList()), model)));
var l = new Button(entry.getPath(), graphic.createRegion());
l.setGraphicTextGap(1);
l.setOnAction(event -> {

View file

@ -0,0 +1,288 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.BrowserTransferProgress;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.store.FileSystem;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Path;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
public class BrowserFileTransferOperation {
private final FileSystem.FileEntry target;
private final List<FileSystem.FileEntry> files;
private final boolean explicitCopy;
private final boolean checkConflicts;
private final Consumer<BrowserTransferProgress> progress;
BrowserAlerts.FileConflictChoice lastConflictChoice;
public BrowserFileTransferOperation(FileSystem.FileEntry target, List<FileSystem.FileEntry> files, boolean explicitCopy, boolean checkConflicts,
Consumer<BrowserTransferProgress> progress
) {
this.target = target;
this.files = files;
this.explicitCopy = explicitCopy;
this.checkConflicts = checkConflicts;
this.progress = progress;
}
public static BrowserFileTransferOperation ofLocal(FileSystem.FileEntry target, List<Path> files, boolean explicitCopy, boolean checkConflicts, Consumer<BrowserTransferProgress> progress) {
var entries = files.stream()
.map(path -> {
try {
return LocalFileSystem.getLocalFileEntry(path);
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.toList();
return new BrowserFileTransferOperation(target, entries, explicitCopy, checkConflicts, progress);
}
private void updateProgress(BrowserTransferProgress progress) {
this.progress.accept(progress);
}
private boolean handleChoice(
FileSystem fileSystem,
String target,
boolean multiple)
throws Exception {
if (lastConflictChoice == BrowserAlerts.FileConflictChoice.CANCEL) {
return false;
}
if (lastConflictChoice == BrowserAlerts.FileConflictChoice.REPLACE_ALL) {
return true;
}
if (fileSystem.fileExists(target)) {
if (lastConflictChoice == BrowserAlerts.FileConflictChoice.SKIP_ALL) {
return false;
}
var choice = BrowserAlerts.showFileConflictAlert(target, multiple);
if (choice == BrowserAlerts.FileConflictChoice.CANCEL) {
lastConflictChoice = BrowserAlerts.FileConflictChoice.CANCEL;
return false;
}
if (choice == BrowserAlerts.FileConflictChoice.SKIP) {
return false;
}
if (choice == BrowserAlerts.FileConflictChoice.SKIP_ALL) {
lastConflictChoice = BrowserAlerts.FileConflictChoice.SKIP_ALL;
return false;
}
if (choice == BrowserAlerts.FileConflictChoice.REPLACE_ALL) {
lastConflictChoice = BrowserAlerts.FileConflictChoice.REPLACE_ALL;
return true;
}
}
return true;
}
public void execute()
throws Exception {
if (files.isEmpty()) {
updateProgress(BrowserTransferProgress.empty());
return;
}
var same = files.getFirst().getFileSystem().equals(target.getFileSystem());
if (same && !explicitCopy) {
if (!BrowserAlerts.showMoveAlert(files, target)) {
return;
}
}
for (var file : files) {
if (file.getFileSystem().equals(target.getFileSystem())) {
handleSingleOnSameFileSystem(file);
updateProgress(BrowserTransferProgress.finished(file.getName(), file.getSize()));
} else {
handleSingleAcrossFileSystems(file);
}
}
}
private void handleSingleOnSameFileSystem(FileSystem.FileEntry source)
throws Exception {
// Prevent dropping directory into itself
if (source.getPath().equals(target.getPath())) {
return;
}
var sourceFile = source.getPath();
var targetFile = FileNames.join(target.getPath(), FileNames.getFileName(sourceFile));
if (sourceFile.equals(targetFile)) {
return;
}
if (source.getKind() == FileKind.DIRECTORY && target.getFileSystem().directoryExists(targetFile)) {
throw ErrorEvent.expected(
new IllegalArgumentException("Target directory " + targetFile + " does already exist"));
}
if (checkConflicts && !handleChoice(target.getFileSystem(), targetFile, files.size() > 1)) {
return;
}
if (explicitCopy) {
target.getFileSystem().copy(sourceFile, targetFile);
} else {
target.getFileSystem().move(sourceFile, targetFile);
}
}
private void handleSingleAcrossFileSystems(FileSystem.FileEntry source)
throws Exception {
if (target.getKind() != FileKind.DIRECTORY) {
throw new IllegalStateException("Target " + target.getPath() + " is not a directory");
}
var flatFiles = new LinkedHashMap<FileSystem.FileEntry, String>();
// Prevent dropping directory into itself
if (source.getFileSystem().equals(target.getFileSystem())
&& FileNames.startsWith(source.getPath(), target.getPath())) {
return;
}
AtomicLong totalSize = new AtomicLong();
if (source.getKind() == FileKind.DIRECTORY) {
var directoryName = FileNames.getFileName(source.getPath());
flatFiles.put(source, directoryName);
var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath()));
List<FileSystem.FileEntry> list = source.getFileSystem().listFilesRecursively(source.getPath());
for (FileSystem.FileEntry fileEntry : list) {
var rel = FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath()));
flatFiles.put(fileEntry, rel);
if (fileEntry.getKind() == FileKind.FILE) {
// This one is up-to-date and does not need to be recalculated
totalSize.addAndGet(fileEntry.getSize());
}
}
} else {
flatFiles.put(source, FileNames.getFileName(source.getPath()));
// Recalculate as it could have been changed meanwhile
totalSize.addAndGet(source.getFileSystem().getFileSize(source.getPath()));
}
AtomicLong transferred = new AtomicLong();
for (var e : flatFiles.entrySet()) {
var sourceFile = e.getKey();
var fixedRelPath = new FilePath(e.getValue())
.fileSystemCompatible(
target.getFileSystem().getShell().orElseThrow().getOsType());
var targetFile = FileNames.join(target.getPath(), fixedRelPath.toString());
if (sourceFile.getFileSystem().equals(target.getFileSystem())) {
throw new IllegalStateException();
}
if (sourceFile.getKind() == FileKind.DIRECTORY) {
target.getFileSystem().mkdirs(targetFile);
} else if (sourceFile.getKind() == FileKind.FILE) {
if (checkConflicts
&& !handleChoice(
target.getFileSystem(),
targetFile,
files.size() > 1 || flatFiles.size() > 1)) {
continue;
}
InputStream inputStream = null;
OutputStream outputStream = null;
try {
var fileSize = sourceFile.getFileSystem().getFileSize(sourceFile.getPath());
inputStream = sourceFile.getFileSystem().openInput(sourceFile.getPath());
outputStream = target.getFileSystem().openOutput(targetFile, fileSize);
transferFile(sourceFile, inputStream, outputStream, transferred, totalSize);
inputStream.transferTo(OutputStream.nullOutputStream());
} catch (Exception ex) {
// Mark progress as finished to reset any progress display
updateProgress(BrowserTransferProgress.finished(sourceFile.getName(), transferred.get()));
if (inputStream != null) {
try {
inputStream.close();
} catch (Exception om) {
// This is expected as the process control has to be killed
// When calling close, it will throw an exception when it has to kill
// ErrorEvent.fromThrowable(om).handle();
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (Exception om) {
// This is expected as the process control has to be killed
// When calling close, it will throw an exception when it has to kill
// ErrorEvent.fromThrowable(om).handle();
}
}
throw ex;
}
updateProgress(BrowserTransferProgress.finished(sourceFile.getName(), transferred.get()));
Exception exception = null;
try {
inputStream.close();
} catch (Exception om) {
exception = om;
}
try {
outputStream.close();
} catch (Exception om) {
if (exception != null) {
ErrorEvent.fromThrowable(om).handle();
} else {
exception = om;
}
}
if (exception != null) {
throw exception;
}
}
}
updateProgress(BrowserTransferProgress.finished(source.getName(), totalSize.get()));
}
private static final int DEFAULT_BUFFER_SIZE = 1024;
private void transferFile(
FileSystem.FileEntry sourceFile,
InputStream inputStream,
OutputStream outputStream,
AtomicLong transferred,
AtomicLong total)
throws IOException {
// Initialize progress immediately prior to reading anything
var now = Instant.now();
updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), now));
var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, sourceFile.getSize());
byte[] buffer = new byte[bs];
int read;
while ((read = inputStream.read(buffer, 0, bs)) > 0) {
outputStream.write(buffer, 0, read);
transferred.addAndGet(read);
updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), now));
}
}
}

View file

@ -63,6 +63,10 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
getItems().clear();
ThreadHelper.runFailableAsync(() -> {
var entry = base.get();
if (entry == null) {
return;
}
if (entry.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY) {
return;
}
@ -97,7 +101,7 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
newItems.add(empty);
} else {
var browserEntries = list.stream()
.map(fileEntry -> new BrowserEntry(fileEntry, model.getFileList(), false))
.map(fileEntry -> new BrowserEntry(fileEntry, model.getFileList()))
.toList();
var menus = browserEntries.stream()
.sorted(model.getFileList().order())
@ -126,10 +130,8 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
this.browserEntry = browserEntry;
this.menu = new Menu(
// Use original name, not the link target
browserEntry.getRawFileEntry().getName(),
PrettyImageHelper.ofFixedSizeSquare(
FileIconManager.getFileIcon(browserEntry.getRawFileEntry(), false), 24)
.createRegion());
browserEntry.getRawFileEntry().getName(), PrettyImageHelper.ofFixedRasterized(
FileIconManager.getFileIcon(browserEntry.getRawFileEntry(), false), 24, 24).createRegion());
createMenu();
addInputListeners();
}

View file

@ -1,28 +1,17 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.browser.BrowserTransferProgress;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.*;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
public class FileSystemHelper {
private static final int DEFAULT_BUFFER_SIZE = 1024;
private static FileSystem localFileSystem;
public static String adjustPath(OpenFileSystemModel model, String path) {
if (path == null) {
return null;
@ -134,23 +123,6 @@ public class FileSystemHelper {
}
}
public static FileSystem.FileEntry getLocal(Path file) throws Exception {
if (localFileSystem == null) {
localFileSystem = new LocalStore().createFileSystem();
localFileSystem.open();
}
return new FileSystem.FileEntry(
localFileSystem,
file.toString(),
Files.getLastModifiedTime(file).toInstant(),
Files.isHidden(file),
Files.isExecutable(file),
Files.size(file),
null,
Files.isDirectory(file) ? FileKind.DIRECTORY : FileKind.FILE);
}
public static FileSystem.FileEntry getRemoteWrapper(FileSystem fileSystem, String file) throws Exception {
return new FileSystem.FileEntry(
fileSystem,
@ -163,24 +135,6 @@ public class FileSystemHelper {
fileSystem.directoryExists(file) ? FileKind.DIRECTORY : FileKind.FILE);
}
public static void dropLocalFilesInto(
FileSystem.FileEntry entry,
List<Path> files,
Consumer<BrowserTransferProgress> progress,
boolean checkConflicts)
throws Exception {
var entries = files.stream()
.map(path -> {
try {
return getLocal(path);
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.toList();
dropFilesInto(entry, entries, false, checkConflicts, progress);
}
public static void delete(List<FileSystem.FileEntry> files) {
if (files.isEmpty()) {
return;
@ -194,255 +148,4 @@ public class FileSystemHelper {
}
}
}
public static void dropFilesInto(
FileSystem.FileEntry target,
List<FileSystem.FileEntry> files,
boolean explicitCopy,
boolean checkConflicts,
Consumer<BrowserTransferProgress> progress)
throws Exception {
if (files.isEmpty()) {
progress.accept(BrowserTransferProgress.empty());
return;
}
var same = files.getFirst().getFileSystem().equals(target.getFileSystem());
if (same && !explicitCopy) {
if (!BrowserAlerts.showMoveAlert(files, target)) {
return;
}
}
AtomicReference<BrowserAlerts.FileConflictChoice> lastConflictChoice = new AtomicReference<>();
for (var file : files) {
if (file.getFileSystem().equals(target.getFileSystem())) {
dropFileAcrossSameFileSystem(
target, file, explicitCopy, lastConflictChoice, files.size() > 1, checkConflicts);
progress.accept(BrowserTransferProgress.finished(file.getName(), file.getSize()));
} else {
dropFileAcrossFileSystems(target, file, progress, lastConflictChoice, files.size() > 1, checkConflicts);
}
}
}
private static void dropFileAcrossSameFileSystem(
FileSystem.FileEntry target,
FileSystem.FileEntry source,
boolean explicitCopy,
AtomicReference<BrowserAlerts.FileConflictChoice> lastConflictChoice,
boolean multiple,
boolean checkConflicts)
throws Exception {
// Prevent dropping directory into itself
if (source.getPath().equals(target.getPath())) {
return;
}
var sourceFile = source.getPath();
var targetFile = FileNames.join(target.getPath(), FileNames.getFileName(sourceFile));
if (sourceFile.equals(targetFile)) {
return;
}
if (source.getKind() == FileKind.DIRECTORY && target.getFileSystem().directoryExists(targetFile)) {
throw ErrorEvent.expected(
new IllegalArgumentException("Target directory " + targetFile + " does already exist"));
}
if (checkConflicts && !handleChoice(lastConflictChoice, target.getFileSystem(), targetFile, multiple)) {
return;
}
if (explicitCopy) {
target.getFileSystem().copy(sourceFile, targetFile);
} else {
target.getFileSystem().move(sourceFile, targetFile);
}
}
private static void dropFileAcrossFileSystems(
FileSystem.FileEntry target,
FileSystem.FileEntry source,
Consumer<BrowserTransferProgress> progress,
AtomicReference<BrowserAlerts.FileConflictChoice> lastConflictChoice,
boolean multiple,
boolean checkConflicts)
throws Exception {
if (target.getKind() != FileKind.DIRECTORY) {
throw new IllegalStateException("Target " + target.getPath() + " is not a directory");
}
var flatFiles = new LinkedHashMap<FileSystem.FileEntry, String>();
// Prevent dropping directory into itself
if (source.getFileSystem().equals(target.getFileSystem())
&& FileNames.startsWith(source.getPath(), target.getPath())) {
return;
}
AtomicLong totalSize = new AtomicLong();
if (source.getKind() == FileKind.DIRECTORY) {
var directoryName = FileNames.getFileName(source.getPath());
flatFiles.put(source, directoryName);
var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath()));
List<FileSystem.FileEntry> list = source.getFileSystem().listFilesRecursively(source.getPath());
for (FileSystem.FileEntry fileEntry : list) {
var rel = FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath()));
flatFiles.put(fileEntry, rel);
if (fileEntry.getKind() == FileKind.FILE) {
// This one is up-to-date and does not need to be recalculated
totalSize.addAndGet(fileEntry.getSize());
}
}
} else {
flatFiles.put(source, FileNames.getFileName(source.getPath()));
// Recalculate as it could have been changed meanwhile
totalSize.addAndGet(source.getFileSystem().getFileSize(source.getPath()));
}
AtomicLong transferred = new AtomicLong();
for (var e : flatFiles.entrySet()) {
var sourceFile = e.getKey();
var fixedRelPath = new FilePath(e.getValue())
.fileSystemCompatible(
target.getFileSystem().getShell().orElseThrow().getOsType());
var targetFile = FileNames.join(target.getPath(), fixedRelPath.toString());
if (sourceFile.getFileSystem().equals(target.getFileSystem())) {
throw new IllegalStateException();
}
if (sourceFile.getKind() == FileKind.DIRECTORY) {
target.getFileSystem().mkdirs(targetFile);
} else if (sourceFile.getKind() == FileKind.FILE) {
if (checkConflicts
&& !handleChoice(
lastConflictChoice,
target.getFileSystem(),
targetFile,
multiple || flatFiles.size() > 1)) {
continue;
}
InputStream inputStream = null;
OutputStream outputStream = null;
try {
var fileSize = sourceFile.getFileSystem().getFileSize(sourceFile.getPath());
inputStream = sourceFile.getFileSystem().openInput(sourceFile.getPath());
outputStream = target.getFileSystem().openOutput(targetFile, fileSize);
transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, progress);
inputStream.transferTo(OutputStream.nullOutputStream());
} catch (Exception ex) {
// Mark progress as finished to reset any progress display
progress.accept(BrowserTransferProgress.finished(sourceFile.getName(), transferred.get()));
if (inputStream != null) {
try {
inputStream.close();
} catch (Exception om) {
// This is expected as the process control has to be killed
// When calling close, it will throw an exception when it has to kill
// ErrorEvent.fromThrowable(om).handle();
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (Exception om) {
// This is expected as the process control has to be killed
// When calling close, it will throw an exception when it has to kill
// ErrorEvent.fromThrowable(om).handle();
}
}
throw ex;
}
progress.accept(BrowserTransferProgress.finished(sourceFile.getName(), transferred.get()));
Exception exception = null;
try {
inputStream.close();
} catch (Exception om) {
exception = om;
}
try {
outputStream.close();
} catch (Exception om) {
if (exception != null) {
ErrorEvent.fromThrowable(om).handle();
} else {
exception = om;
}
}
if (exception != null) {
throw exception;
}
}
}
progress.accept(BrowserTransferProgress.finished(source.getName(), totalSize.get()));
}
private static boolean handleChoice(
AtomicReference<BrowserAlerts.FileConflictChoice> previous,
FileSystem fileSystem,
String target,
boolean multiple)
throws Exception {
if (previous.get() == BrowserAlerts.FileConflictChoice.CANCEL) {
return false;
}
if (previous.get() == BrowserAlerts.FileConflictChoice.REPLACE_ALL) {
return true;
}
if (fileSystem.fileExists(target)) {
if (previous.get() == BrowserAlerts.FileConflictChoice.SKIP_ALL) {
return false;
}
var choice = BrowserAlerts.showFileConflictAlert(target, multiple);
if (choice == BrowserAlerts.FileConflictChoice.CANCEL) {
previous.set(BrowserAlerts.FileConflictChoice.CANCEL);
return false;
}
if (choice == BrowserAlerts.FileConflictChoice.SKIP) {
return false;
}
if (choice == BrowserAlerts.FileConflictChoice.SKIP_ALL) {
previous.set(BrowserAlerts.FileConflictChoice.SKIP_ALL);
return false;
}
if (choice == BrowserAlerts.FileConflictChoice.REPLACE_ALL) {
previous.set(BrowserAlerts.FileConflictChoice.REPLACE_ALL);
return true;
}
}
return true;
}
private static void transferFile(
FileSystem.FileEntry sourceFile,
InputStream inputStream,
OutputStream outputStream,
AtomicLong transferred,
AtomicLong total,
Consumer<BrowserTransferProgress> progress)
throws IOException {
// Initialize progress immediately prior to reading anything
progress.accept(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get()));
var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, sourceFile.getSize());
byte[] buffer = new byte[bs];
int read;
while ((read = inputStream.read(buffer, 0, bs)) > 0) {
outputStream.write(buffer, 0, read);
transferred.addAndGet(read);
progress.accept(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get()));
}
}
}

View file

@ -0,0 +1,42 @@
package io.xpipe.app.browser.file;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileSystem;
import io.xpipe.core.store.LocalStore;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class LocalFileSystem {
private static FileSystem localFileSystem;
public static void init() throws Exception {
if (localFileSystem == null) {
localFileSystem = new LocalStore().createFileSystem();
localFileSystem.open();
}
}
public static FileSystem.FileEntry getLocalFileEntry(Path file) throws IOException {
if (localFileSystem == null) {
throw new IllegalStateException();
}
return new FileSystem.FileEntry(
localFileSystem,
file.toString(),
Files.getLastModifiedTime(file).toInstant(),
Files.isHidden(file),
Files.isExecutable(file),
Files.size(file),
null,
Files.isDirectory(file) ? FileKind.DIRECTORY : FileKind.FILE);
}
public static BrowserEntry getLocalBrowserEntry(Path file) throws Exception {
var e = getLocalFileEntry(file);
return new BrowserEntry(e,null);
}
}

View file

@ -4,6 +4,7 @@ import io.xpipe.app.browser.BrowserSavedState;
import io.xpipe.app.browser.BrowserTransferProgress;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.file.BrowserFileListModel;
import io.xpipe.app.browser.file.BrowserFileTransferOperation;
import io.xpipe.app.browser.file.FileSystemHelper;
import io.xpipe.app.browser.session.BrowserAbstractSessionModel;
import io.xpipe.app.browser.session.BrowserSessionModel;
@ -297,11 +298,17 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
loadFilesSync(path);
}
public void withFiles(String dir, FailableConsumer<Stream<FileSystem.FileEntry>, Exception> consumer) throws Exception {
public void withFiles(String dir, FailableConsumer<Stream<FileSystem.FileEntry>, Exception> consumer)
throws Exception {
BooleanScope.executeExclusive(busy, () -> {
if (dir != null) {
startIfNeeded();
var stream = getFileSystem().listFiles(dir);
var fs = getFileSystem();
if (fs == null) {
return;
}
var stream = fs.listFiles(dir);
consumer.accept(stream);
} else {
consumer.accept(Stream.of());
@ -311,9 +318,10 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
private boolean loadFilesSync(String dir) {
try {
if (dir != null) {
startIfNeeded();
var stream = getFileSystem().listFiles(dir);
startIfNeeded();
var fs = getFileSystem();
if (dir != null && fs != null) {
var stream = fs.listFiles(dir);
fileList.setAll(stream);
} else {
fileList.setAll(Stream.of());
@ -334,7 +342,8 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
}
startIfNeeded();
FileSystemHelper.dropLocalFilesInto(entry, files, progress::setValue, true);
var op = BrowserFileTransferOperation.ofLocal(entry, files,false,true, progress::setValue);
op.execute();
refreshSync();
});
});
@ -354,9 +363,8 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
}
startIfNeeded();
FileSystemHelper.dropFilesInto(target, files, explicitCopy, true, browserTransferProgress -> {
progress.setValue(browserTransferProgress);
});
var op = new BrowserFileTransferOperation(target, files,false,true, progress::setValue);
op.execute();
refreshSync();
});
});

View file

@ -11,6 +11,8 @@ public class FileIconManager {
public static synchronized void loadIfNecessary() {
if (!loaded) {
BrowserIconFileType.loadDefinitions();
BrowserIconDirectoryType.loadDefinitions();
AppImages.loadDirectory(AppResources.XPIPE_MODULE, "browser_icons", true, false);
loaded = true;
}

View file

@ -4,6 +4,7 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
@ -46,7 +47,7 @@ public class ButtonComp extends Comp<CompStructure<Button>> {
public CompStructure<Button> createBase() {
var button = new Button(null);
if (name != null) {
button.textProperty().bind(name);
button.textProperty().bind(PlatformThread.sync(name));
}
var graphic = getGraphic();
if (graphic instanceof FontIcon f) {

View file

@ -2,7 +2,6 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.AppLogs;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
@ -10,8 +9,6 @@ import io.xpipe.app.fxcomps.augment.Augment;
import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.UserReportComp;
import io.xpipe.app.update.UpdateAvailableAlert;
import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.Hyperlinks;
@ -125,27 +122,9 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
noneBorder));
};
{
var b = new IconButtonComp("mdal-bug_report", () -> {
var event = ErrorEvent.fromMessage("User Report");
if (AppLogs.get().isWriteToFile()) {
event.attachment(AppLogs.get().getSessionLogsDirectory());
}
UserReportComp.show(event.build());
})
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size()]))
.apply(new TooltipAugment<>("reportIssue"))
.apply(simpleBorders)
.accessibleTextKey("reportIssue");
b.apply(struc -> {
AppFont.setSize(struc.get(), 2);
});
vbox.getChildren().add(b.createRegion());
}
{
var b = new IconButtonComp("mdi2g-github", () -> Hyperlinks.open(Hyperlinks.GITHUB))
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 1]))
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size()]))
.apply(new TooltipAugment<>("visitGithubRepository"))
.apply(simpleBorders)
.accessibleTextKey("visitGithubRepository");
@ -157,7 +136,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
{
var b = new IconButtonComp("mdi2d-discord", () -> Hyperlinks.open(Hyperlinks.DISCORD))
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 2]))
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 1]))
.apply(new TooltipAugment<>("discord"))
.apply(simpleBorders)
.accessibleTextKey("discord");
@ -169,7 +148,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
{
var b = new IconButtonComp("mdi2t-translate", () -> Hyperlinks.open(Hyperlinks.TRANSLATE))
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 3]))
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 2]))
.apply(new TooltipAugment<>("translate"))
.apply(simpleBorders)
.accessibleTextKey("translate");

View file

@ -6,11 +6,13 @@ import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.DataStore;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.scene.layout.Region;
import lombok.AllArgsConstructor;
import lombok.Setter;
@ -29,17 +31,29 @@ public class StoreToggleComp extends SimpleComp {
@Setter
private ObservableBooleanValue customVisibility = new SimpleBooleanProperty(true);
public static <T extends DataStore> StoreToggleComp simpleToggle(String nameKey, StoreSection section, Function<T, Boolean> initial, BiConsumer<T, Boolean> setter) {
return new StoreToggleComp(nameKey, section,new SimpleBooleanProperty(initial.apply(section.getWrapper().getEntry().getStore().asNeeded())),v -> {
setter.accept(section.getWrapper().getEntry().getStore().asNeeded(),v);
});
public static <T extends DataStore> StoreToggleComp simpleToggle(
String nameKey, StoreSection section, Function<T, Boolean> initial, BiConsumer<T, Boolean> setter) {
return new StoreToggleComp(
nameKey,
section,
new SimpleBooleanProperty(
initial.apply(section.getWrapper().getEntry().getStore().asNeeded())),
v -> {
setter.accept(section.getWrapper().getEntry().getStore().asNeeded(), v);
});
}
public static <T extends DataStore> StoreToggleComp childrenToggle(String nameKey, StoreSection section, Function<T, Boolean> initial, BiConsumer<T, Boolean> setter) {
return new StoreToggleComp(nameKey, section,new SimpleBooleanProperty(initial.apply(section.getWrapper().getEntry().getStore().asNeeded())),v -> {
setter.accept(section.getWrapper().getEntry().getStore().asNeeded(),v);
section.getWrapper().refreshChildren();
});
public static <T extends DataStore> StoreToggleComp childrenToggle(
String nameKey, StoreSection section, Function<T, Boolean> initial, BiConsumer<T, Boolean> setter) {
return new StoreToggleComp(
nameKey,
section,
new SimpleBooleanProperty(
initial.apply(section.getWrapper().getEntry().getStore().asNeeded())),
v -> {
setter.accept(section.getWrapper().getEntry().getStore().asNeeded(), v);
section.getWrapper().refreshChildren();
});
}
public StoreToggleComp(String nameKey, StoreSection section, boolean initial, Consumer<Boolean> onChange) {
@ -65,8 +79,7 @@ public class StoreToggleComp extends SimpleComp {
return false;
}
return section.getWrapper().getValidity().getValue() == DataStoreEntry.Validity.COMPLETE
&& section.getShowDetails().get();
return section.getWrapper().getValidity().getValue() == DataStoreEntry.Validity.COMPLETE;
},
section.getWrapper().getValidity(),
section.getShowDetails(),

View file

@ -5,10 +5,12 @@ import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.util.ScanAlert;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import org.kordamp.ikonli.javafx.FontIcon;
public class StoreCreationMenu {
@ -83,7 +85,9 @@ public class StoreCreationMenu {
category);
event.consume();
});
var providers = sub.stream().sorted((o1, o2) -> -o1.getModuleName().compareTo(o2.getModuleName())).toList();
var providers = sub.stream()
.sorted((o1, o2) -> -o1.getModuleName().compareTo(o2.getModuleName()))
.toList();
for (int i = 0; i < providers.size(); i++) {
var dataStoreProvider = providers.get(i);
if (i > 0 && !providers.get(i - 1).getModuleName().equals(dataStoreProvider.getModuleName())) {

View file

@ -116,7 +116,9 @@ public abstract class StoreEntryComp extends SimpleComp {
var loading = LoadingOverlayComp.noProgress(
Comp.of(() -> button),
wrapper.getBusy().or(wrapper.getEntry().getProvider().busy(wrapper)));
wrapper.getEntry().getValidity().isUsable()
? wrapper.getBusy().or(wrapper.getEntry().getProvider().busy(wrapper))
: wrapper.getBusy());
return loading.createRegion();
}
@ -377,7 +379,7 @@ public abstract class StoreEntryComp extends SimpleComp {
if (AppPrefs.get().developerMode().getValue()) {
var browse = new MenuItem(AppI18n.get("browseInternalStorage"), new FontIcon("mdi2f-folder-open-outline"));
browse.setOnAction(
event -> DesktopHelper.browsePath(wrapper.getEntry().getDirectory()));
event -> DesktopHelper.browsePathLocal(wrapper.getEntry().getDirectory()));
contextMenu.getItems().add(browse);
}

View file

@ -68,6 +68,10 @@ public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> {
m.getItems().setAll(items);
m.setOnAction(event -> {
if (event.getTarget() == m) {
if (m.getItems().isEmpty()) {
return;
}
action.accept(w);
contextMenu.hide();
event.consume();

View file

@ -2,12 +2,10 @@ package io.xpipe.app.core;
import io.xpipe.app.comp.AppLayoutComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.LicenseProvider;
import io.xpipe.core.process.OsType;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
@ -16,8 +14,6 @@ import javafx.stage.Stage;
import lombok.Getter;
import lombok.SneakyThrows;
import java.awt.*;
@Getter
public class App extends Application {
@ -35,25 +31,6 @@ public class App extends Application {
APP = this;
stage = primaryStage;
stage.opacityProperty().bind(AppPrefs.get().windowOpacity());
if (OsType.getLocal().equals(OsType.MACOS)) {
Desktop.getDesktop().setPreferencesHandler(e -> {
AppLayoutModel.get().selectSettings();
});
}
if (OsType.getLocal().equals(OsType.LINUX)) {
try {
Toolkit xToolkit = Toolkit.getDefaultToolkit();
java.lang.reflect.Field awtAppClassNameField =
xToolkit.getClass().getDeclaredField("awtAppClassName");
awtAppClassNameField.setAccessible(true);
awtAppClassNameField.set(xToolkit, "XPipe");
} catch (Exception e) {
ErrorEvent.fromThrowable(e).omit().handle();
}
}
AppWindowHelper.addIcons(stage);
}

View file

@ -8,6 +8,7 @@ import io.xpipe.core.process.ProcessControlProvider;
import io.xpipe.core.util.ModuleHelper;
import io.xpipe.core.util.ModuleLayerLoader;
import io.xpipe.core.util.XPipeInstallation;
import lombok.Getter;
import lombok.Value;
@ -95,12 +96,16 @@ public class AppExtensionManager {
}
var iv = getLocalInstallVersion();
var installVersion = AppVersion.parse(iv).orElseThrow(() -> new IllegalArgumentException("Invalid installation version: " + iv));
var sv = !AppProperties.get().isImage() ? Files.readString(Path.of("version")).trim() : AppProperties.get().getVersion();
var sourceVersion = AppVersion.parse(sv).orElseThrow(() -> new IllegalArgumentException("Invalid source version: " + sv));
var installVersion = AppVersion.parse(iv)
.orElseThrow(() -> new IllegalArgumentException("Invalid installation version: " + iv));
var sv = !AppProperties.get().isImage()
? Files.readString(Path.of("version")).trim()
: AppProperties.get().getVersion();
var sourceVersion = AppVersion.parse(sv)
.orElseThrow(() -> new IllegalArgumentException("Invalid source version: " + sv));
if (!installVersion.equals(sourceVersion)) {
throw new IllegalStateException(
"Incompatible development version. Source: " + iv + ", Installation: " + sv + "\n\nPlease try to check out the matching release version in the repository.");
throw new IllegalStateException("Incompatible development version. Source: " + iv + ", Installation: "
+ sv + "\n\nPlease try to check out the matching release version in the repository.");
}
var extensions = XPipeInstallation.getLocalExtensionsDirectory(p);
@ -171,8 +176,8 @@ public class AppExtensionManager {
if (loadedExtensions.stream().anyMatch(extension -> dir.equals(extension.dir))
|| loadedExtensions.stream()
.anyMatch(extension ->
extension.id.equals(dir.getFileName().toString()))) {
.anyMatch(extension ->
extension.id.equals(dir.getFileName().toString()))) {
return Optional.empty();
}

View file

@ -6,21 +6,17 @@ import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.prefs.SupportedLocale;
import io.xpipe.app.util.OptionsBuilder;
import io.xpipe.app.util.Translatable;
import io.xpipe.core.util.ModuleHelper;
import io.xpipe.core.util.XPipeInstallation;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import lombok.SneakyThrows;
import lombok.Value;
import org.apache.commons.io.FilenameUtils;
import org.ocpsoft.prettytime.PrettyTime;
import java.io.IOException;
import java.io.InputStreamReader;
@ -90,6 +86,10 @@ public class AppI18n {
private static String getCallerModuleName() {
var callers = CallingClass.INSTANCE.getCallingClasses();
for (Class<?> caller : callers) {
if (caller.isSynthetic()) {
continue;
}
if (caller.equals(CallingClass.class)
|| caller.equals(ModuleHelper.class)
|| caller.equals(ModalOverlayComp.class)
@ -127,7 +127,7 @@ public class AppI18n {
}
}
private LoadedTranslations getLoaded() {
public LoadedTranslations getLoaded() {
return currentLanguage.getValue() != null ? currentLanguage.getValue() : english;
}
@ -274,21 +274,15 @@ public class AppI18n {
});
}
var prettyTime = new PrettyTime(
AppPrefs.get() != null
? AppPrefs.get().language().getValue().getLocale()
: SupportedLocale.getEnglish().getLocale());
return new LoadedTranslations(l, translations, markdownDocumentations, prettyTime);
return new LoadedTranslations(l, translations, markdownDocumentations);
}
@Value
static class LoadedTranslations {
public static class LoadedTranslations {
Locale locale;
Map<String, String> translations;
Map<String, String> markdownDocumentations;
PrettyTime prettyTime;
}
@SuppressWarnings("removal")

View file

@ -0,0 +1,97 @@
package io.xpipe.app.core;
import io.xpipe.app.Main;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.launcher.LauncherInput;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.OsType;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.desktop.*;
import java.util.List;
public class AppIntegration {
public static void setupDesktopIntegrations() {
try {
if (Desktop.isDesktopSupported()) {
Desktop.getDesktop().addAppEventListener(new SystemSleepListener() {
@Override
public void systemAboutToSleep(SystemSleepEvent e) {
if (AppPrefs.get() != null &&
AppPrefs.get().lockVaultOnHibernation().get() &&
AppPrefs.get().getLockCrypt().get() != null &&
!AppPrefs.get().getLockCrypt().get().isBlank()) {
// If we run this at the same time as the system is sleeping, there might be exceptions
// because the platform does not like being shut down while sleeping
// This hopefully assures that it will be run later, probably on system wake
ThreadHelper.runAsync(() -> {
ThreadHelper.sleep(1000);
OperationMode.close();
});
}
}
@Override
public void systemAwoke(SystemSleepEvent e) {}
});
}
// This will initialize the toolkit on macos and create the dock icon
// macOS does not like applications that run fully in the background, so always do it
if (OsType.getLocal().equals(OsType.MACOS)) {
Desktop.getDesktop().setPreferencesHandler(e -> {
AppLayoutModel.get().selectSettings();
});
// URL open operations have to be handled in a special way on macOS!
Desktop.getDesktop().setOpenURIHandler(e -> {
LauncherInput.handle(List.of(e.getURI().toString()));
});
// Do it this way to prevent IDE inspections from complaining
var c = Class.forName(
ModuleLayer.boot().findModule("java.desktop").orElseThrow(), "com.apple.eawt.Application");
var m = c.getDeclaredMethod("addAppEventListener", SystemEventListener.class);
m.invoke(c.getMethod("getApplication").invoke(null), new AppReopenedListener() {
@Override
public void appReopened(AppReopenedEvent e) {
OperationMode.switchToAsync(OperationMode.GUI);
}
});
// Set dock icon explicitly on mac
// This is necessary in case XPipe was started through a script as it will have no icon otherwise
if (AppProperties.get().isDeveloperMode() && AppLogs.get().isWriteToSysout()) {
try {
var iconUrl = Main.class.getResourceAsStream("resources/img/logo/padded/logo_128x128.png");
if (iconUrl != null) {
var awtIcon = ImageIO.read(iconUrl);
Taskbar.getTaskbar().setIconImage(awtIcon);
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).omitted(true).build().handle();
}
}
}
if (OsType.getLocal().equals(OsType.LINUX)) {
try {
Toolkit xToolkit = Toolkit.getDefaultToolkit();
java.lang.reflect.Field awtAppClassNameField =
xToolkit.getClass().getDeclaredField("awtAppClassName");
awtAppClassNameField.setAccessible(true);
awtAppClassNameField.set(xToolkit, "XPipe");
} catch (Exception e) {
ErrorEvent.fromThrowable(e).omit().handle();
}
}
} catch (Throwable ex) {
ErrorEvent.fromThrowable(ex).term().handle();
}
}
}

View file

@ -55,7 +55,9 @@ public class AppProperties {
.orElse(UUID.randomUUID());
sentryUrl = System.getProperty("io.xpipe.app.sentryUrl");
arch = System.getProperty("io.xpipe.app.arch");
languages = Arrays.stream(System.getProperty("io.xpipe.app.languages").split(";")).sorted().toList();
languages = Arrays.stream(System.getProperty("io.xpipe.app.languages").split(","))
.sorted()
.toList();
staging = XPipeInstallation.isStaging();
useVirtualThreads = Optional.ofNullable(System.getProperty("io.xpipe.app.useVirtualThreads"))
.map(Boolean::parseBoolean)

View file

@ -65,7 +65,7 @@ public class AppAvCheck {
alert.getDialogPane().setPadding(new Insets(15));
});
alert.getButtonTypes().add(new ButtonType(AppI18n.get("gotIt"), ButtonBar.ButtonData.OK_DONE));
alert.getButtonTypes().add(new ButtonType(AppI18n.get("ok"), ButtonBar.ButtonData.OK_DONE));
});
a.filter(b -> b.getButtonData().isDefaultButton())
.ifPresentOrElse(buttonType -> {}, () -> OperationMode.halt(1));

View file

@ -54,8 +54,8 @@ public class AppShellCheck {
The most likely causes are:
- On Windows, an AntiVirus program might block required programs and commands
- The system shell is restricted or blocked
- Your PATH environment variable is corrupt / incomplete. You can check this by manually trying to run some commands in a terminal
- Some elementary command-line tools are not available or not working correctly
- Your PATH environment variable is corrupt / incomplete
%s
"""

View file

@ -1,7 +1,6 @@
package io.xpipe.app.core.mode;
import io.xpipe.app.browser.icon.BrowserIconDirectoryType;
import io.xpipe.app.browser.icon.BrowserIconFileType;
import io.xpipe.app.browser.file.LocalFileSystem;
import io.xpipe.app.browser.icon.FileIconManager;
import io.xpipe.app.core.App;
import io.xpipe.app.core.AppGreetings;
@ -12,7 +11,6 @@ import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.update.UpdateChangelogAlert;
import io.xpipe.app.util.ThreadHelper;
import javafx.stage.Stage;
public class GuiMode extends PlatformMode {
@ -52,11 +50,13 @@ public class GuiMode extends PlatformMode {
});
TrackEvent.info("Window setup complete");
ThreadHelper.runAsync(() -> {
BrowserIconFileType.loadDefinitions();
BrowserIconDirectoryType.loadDefinitions();
// Can be loaded async
ThreadHelper.runFailableAsync(() -> {
FileIconManager.loadIfNecessary();
});
ThreadHelper.runFailableAsync(() -> {
LocalFileSystem.init();
});
UpdateChangelogAlert.showIfNeeded();
}

View file

@ -1,32 +1,23 @@
package io.xpipe.app.core.mode;
import io.xpipe.app.Main;
import io.xpipe.app.core.App;
import io.xpipe.app.core.AppLogs;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.AppState;
import io.xpipe.app.core.*;
import io.xpipe.app.core.check.AppDebugModeCheck;
import io.xpipe.app.core.check.AppTempCheck;
import io.xpipe.app.core.check.AppUserDirectoryCheck;
import io.xpipe.app.issue.*;
import io.xpipe.app.launcher.LauncherCommand;
import io.xpipe.app.launcher.LauncherInput;
import io.xpipe.app.util.LocalShell;
import io.xpipe.app.util.PlatformState;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.app.util.XPipeSession;
import io.xpipe.core.process.OsType;
import io.xpipe.core.util.FailableRunnable;
import io.xpipe.core.util.XPipeDaemonMode;
import io.xpipe.core.util.XPipeInstallation;
import javafx.application.Platform;
import lombok.Getter;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.desktop.AppReopenedEvent;
import java.awt.desktop.AppReopenedListener;
import java.awt.desktop.SystemEventListener;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
@ -130,47 +121,7 @@ public abstract class OperationMode {
setup(args);
LauncherCommand.runLauncher(usedArgs);
inStartup = false;
postInit(args);
}
public static void postInit(String[] args) {
try {
// This will initialize the toolkit on macos and create the dock icon
// macOS does not like applications that run fully in the background, so always do it
if (OsType.getLocal().equals(OsType.MACOS)) {
// URL open operations have to be handled in a special way on macOS!
Desktop.getDesktop().setOpenURIHandler(e -> {
LauncherInput.handle(List.of(e.getURI().toString()));
});
// Do it this way to prevent IDE inspections from complaining
var c = Class.forName(
ModuleLayer.boot().findModule("java.desktop").orElseThrow(), "com.apple.eawt.Application");
var m = c.getDeclaredMethod("addAppEventListener", SystemEventListener.class);
m.invoke(c.getMethod("getApplication").invoke(null), new AppReopenedListener() {
@Override
public void appReopened(AppReopenedEvent e) {
OperationMode.switchToAsync(OperationMode.GUI);
}
});
// Set dock icon explicitly on mac
// This is necessary in case XPipe was started through a script as it will have no icon otherwise
if (AppProperties.get().isDeveloperMode() && AppLogs.get().isWriteToSysout()) {
try {
var iconUrl = Main.class.getResourceAsStream("resources/img/logo/padded/logo_128x128.png");
if (iconUrl != null) {
var awtIcon = ImageIO.read(iconUrl);
Taskbar.getTaskbar().setIconImage(awtIcon);
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).omitted(true).build().handle();
}
}
}
} catch (Throwable ex) {
ErrorEvent.fromThrowable(ex).term().handle();
}
AppIntegration.setupDesktopIntegrations();
}
public static void switchToAsync(OperationMode newMode) {

View file

@ -5,6 +5,7 @@ import io.xpipe.app.fxcomps.augment.Augment;
import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.Shortcuts;
import javafx.application.Platform;
@ -148,13 +149,15 @@ public abstract class Comp<S extends CompStructure<?>> {
var region = struc.get();
BindingsHelper.preserve(region, o);
o.subscribe(n -> {
if (!n) {
region.setVisible(true);
region.setManaged(true);
} else {
region.setVisible(false);
region.setManaged(false);
}
PlatformThread.runLaterIfNeeded(() -> {
if (!n) {
region.setVisible(true);
region.setManaged(true);
} else {
region.setVisible(false);
region.setManaged(false);
}
});
});
});
}

View file

@ -206,7 +206,12 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
16);
struc.get().setGraphic(graphic.createRegion());
struc.get().setOnAction(event -> {
getPopover().show(struc.get());
if (popover == null || !popover.isShowing()) {
var p = getPopover();
p.show(struc.get());
} else {
popover.hide();
}
event.consume();
});
})

View file

@ -28,6 +28,15 @@ public class PrettyImageHelper {
return ofFixedSize(img, size, size);
}
public static Comp<?> ofFixedRasterized(String img, int w, int h) {
if (img == null) {
return new PrettyImageComp(new SimpleStringProperty(null), w, h);
}
var rasterized = rasterizedImageIfExists(img, w, h);
return new PrettyImageComp(new SimpleStringProperty(rasterized.orElse(null)), w, h);
}
public static Comp<?> ofFixedSize(String img, int w, int h) {
if (img == null) {
return new PrettyImageComp(new SimpleStringProperty(null), w, h);

View file

@ -1,18 +1,20 @@
package io.xpipe.app.fxcomps.impl;
import atlantafx.base.layout.InputGroup;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.ClipboardHelper;
import io.xpipe.core.util.InPlaceSecretValue;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import atlantafx.base.layout.InputGroup;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.kordamp.ikonli.javafx.FontIcon;
@ -27,6 +29,7 @@ public class SecretFieldComp extends Comp<SecretFieldComp.Structure> {
public static class Structure implements CompStructure<InputGroup> {
private final InputGroup inputGroup;
@Getter
private final TextField field;
@ -85,8 +88,11 @@ public class SecretFieldComp extends Comp<SecretFieldComp.Structure> {
HBox.setHgrow(text, Priority.ALWAYS);
var copyButton = new ButtonComp(null, new FontIcon("mdi2c-clipboard-multiple-outline"), () -> {
ClipboardHelper.copyPassword(value.getValue());
}).grow(false, true).tooltipKey("copyPassword").createRegion();
ClipboardHelper.copyPassword(value.getValue());
})
.grow(false, true)
.tooltipKey("copyPassword")
.createRegion();
var ig = new InputGroup(text);
ig.getStyleClass().add("secret-field-comp");

View file

@ -23,6 +23,9 @@ public class ErrorEvent {
@Builder.Default
private final boolean reportable = true;
@Setter
private boolean disableDefaultActions;
private final Throwable throwable;
@Singular
@ -153,6 +156,10 @@ public class ErrorEvent {
return omit().expected();
}
public ErrorEventBuilder noDefaultActions() {
return disableDefaultActions(true);
}
public void handle() {
build().handle();
}

View file

@ -11,7 +11,6 @@ import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.util.LicenseRequiredException;
import io.xpipe.app.util.PlatformState;
import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
@ -25,7 +24,6 @@ import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
@ -243,25 +241,24 @@ public class ErrorHandlerComp extends SimpleComp {
return true;
}
});
event.setDisableDefaultActions(true);
}
var custom = event.getCustomActions();
for (var c : custom) {
var ac = createActionComp(c);
ac.getStyleClass().addAll(BUTTON_OUTLINED, ACCENT);
actionBox.getChildren().add(ac);
}
for (var action : List.of(ErrorAction.automaticallyReport(), ErrorAction.reportOnGithub())) {
var ac = createActionComp(action);
actionBox.getChildren().add(ac);
if (!event.isDisableDefaultActions() || event.getCustomActions().isEmpty()) {
for (var action : List.of(ErrorAction.automaticallyReport(), ErrorAction.reportOnGithub(), ErrorAction.ignore())) {
var ac = createActionComp(action);
actionBox.getChildren().add(ac);
}
actionBox.getChildren().get(1).getStyleClass().addAll(BUTTON_OUTLINED, ACCENT);
}
for (var action : List.of(ErrorAction.ignore())) {
var ac = createActionComp(action);
actionBox.getChildren().add(ac);
}
actionBox.getChildren().get(1).getStyleClass().addAll(BUTTON_OUTLINED, ACCENT);
content.getChildren().addAll(actionBox, new Separator(Orientation.HORIZONTAL));
var details = createDetails();

View file

@ -4,8 +4,8 @@ import io.xpipe.app.core.*;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.ThreadHelper;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;

View file

@ -1,7 +1,5 @@
package io.xpipe.app.issue;
import atlantafx.base.controls.Popover;
import atlantafx.base.controls.Spacer;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.ListSelectorComp;
import io.xpipe.app.comp.base.MarkdownComp;
@ -9,6 +7,7 @@ import io.xpipe.app.comp.base.TitledPaneComp;
import io.xpipe.app.core.*;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleStringProperty;
@ -23,6 +22,9 @@ import javafx.scene.control.TextField;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import atlantafx.base.controls.Popover;
import atlantafx.base.controls.Spacer;
import java.nio.file.Files;
import java.nio.file.Path;

View file

@ -96,6 +96,8 @@ public class AppPrefs {
map(new SimpleBooleanProperty(false), "condenseConnectionDisplay", Boolean.class);
final BooleanProperty showChildCategoriesInParentCategory =
map(new SimpleBooleanProperty(true), "showChildrenConnectionsInParentCategory", Boolean.class);
final BooleanProperty lockVaultOnHibernation =
map(new SimpleBooleanProperty(false), "lockVaultOnHibernation", Boolean.class);
final BooleanProperty openConnectionSearchWindowOnConnectionCreation =
map(new SimpleBooleanProperty(true), "openConnectionSearchWindowOnConnectionCreation", Boolean.class);
final ObjectProperty<Path> storageDirectory =
@ -139,21 +141,22 @@ public class AppPrefs {
private AppPrefs() {
this.categories = Stream.of(
new AboutCategory(),
new SystemCategory(),
new AppearanceCategory(),
new SyncCategory(),
new VaultCategory(),
new PasswordManagerCategory(),
new TerminalCategory(),
new EditorCategory(),
new RdpCategory(),
new SshCategory(),
new LocalShellCategory(),
new SecurityCategory(),
new TroubleshootCategory(),
new DeveloperCategory())
.filter(appPrefsCategory -> appPrefsCategory.show()).toList();
new AboutCategory(),
new SystemCategory(),
new AppearanceCategory(),
new SyncCategory(),
new VaultCategory(),
new PasswordManagerCategory(),
new TerminalCategory(),
new EditorCategory(),
new RdpCategory(),
new SshCategory(),
new LocalShellCategory(),
new SecurityCategory(),
new TroubleshootCategory(),
new DeveloperCategory())
.filter(appPrefsCategory -> appPrefsCategory.show())
.toList();
var selected = AppCache.get("selectedPrefsCategory", Integer.class, () -> 0);
if (selected == null) {
selected = 0;
@ -262,6 +265,10 @@ public class AppPrefs {
return disableTerminalRemotePasswordPreparation;
}
public ObservableBooleanValue lockVaultOnHibernation() {
return lockVaultOnHibernation;
}
public ObservableValue<Boolean> alwaysConfirmElevation() {
return alwaysConfirmElevation;
}

View file

@ -11,7 +11,6 @@ import io.xpipe.core.process.ShellControl;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.Optional;
public abstract class ExternalApplicationType implements PrefsChoiceValue {
@ -43,46 +42,17 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue {
this.applicationName = applicationName;
}
protected Optional<Path> getApplicationPath() {
try (ShellControl pc = LocalShell.getShell().start()) {
try (var c = pc.command(String.format(
"/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister "
+ "-dump | grep -o \"/.*%s.app\" | grep -v -E \"Caches|TimeMachine|Temporary|.Trash|/Volumes/%s\" | uniq",
applicationName, applicationName))
.start()) {
var path = c.readStdoutDiscardErr();
if (c.getExitCode() != 0 || path.isBlank()) {
return Optional.empty();
}
// Check if returned paths are actually valid
// Also sort them by length to prevent finding a deeply buried app
var valid = path.lines()
.filter(s -> {
try {
return Files.exists(Path.of(s));
} catch (Exception ex) {
return false;
}
})
.sorted(Comparator.comparingInt(value -> value.length()))
.toList();
// Require app in proper applications directory
var app = valid.stream()
.filter(s -> s.contains("Applications"))
.findFirst();
return app.map(Path::of);
}
} catch (Exception e) {
ErrorEvent.fromThrowable(e).omit().handle();
return Optional.empty();
}
}
@Override
public boolean isAvailable() {
return getApplicationPath().isPresent();
try (ShellControl pc = LocalShell.getShell().start()) {
return pc.command(String.format(
"mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications",
applicationName))
.executeAndCheck();
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
return false;
}
}
@Override

View file

@ -194,14 +194,9 @@ public interface ExternalEditorType extends PrefsChoiceValue {
@Override
public void launch(Path file) throws Exception {
var execFile = getApplicationPath();
if (execFile.isEmpty()) {
throw new IOException("Application " + applicationName + ".app not found");
}
ExternalApplicationHelper.startAsync(CommandBuilder.of()
.add("open", "-a")
.addFile(execFile.orElseThrow().toString())
.addQuoted(applicationName)
.addFile(file.toString()));
}
}

View file

@ -5,8 +5,8 @@ import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.*;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.OsType;
import io.xpipe.core.util.SecretValue;
import lombok.Value;
import java.nio.file.Files;
@ -59,8 +59,8 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
private String encrypt(SecretValue password) throws Exception {
var ps = LocalShell.getLocalPowershell();
var cmd = ps.command(
"(\"" + password.getSecretValue() + "\" | ConvertTo-SecureString -AsPlainText -Force) | ConvertFrom-SecureString;");
var cmd = ps.command("(\"" + password.getSecretValue()
+ "\" | ConvertTo-SecureString -AsPlainText -Force) | ConvertFrom-SecureString;");
cmd.setSensitive();
return cmd.readStdoutOrThrow();
}

View file

@ -27,7 +27,7 @@ public class SyncCategory extends AppPrefsCategory {
.addComp(prefs.getCustomComp("gitVaultIdentityStrategy"))
.nameAndDescription("openDataDir")
.addComp(new ButtonComp(AppI18n.observable("openDataDirButton"), () -> {
DesktopHelper.browsePath(DataStorage.get().getDataDir());
DesktopHelper.browsePathLocal(DataStorage.get().getDataDir());
})));
return builder.buildComp();
}

View file

@ -79,7 +79,7 @@ public class TroubleshootCategory extends AppPrefsCategory {
"openInstallationDirectoryDescription",
"mdomz-snippet_folder",
e -> {
DesktopHelper.browsePath(
DesktopHelper.browsePathLocal(
XPipeInstallation.getCurrentInstallationBasePath());
e.consume();
})

View file

@ -52,7 +52,12 @@ public class VaultCategory extends AppPrefsCategory {
},
prefs.getLockCrypt()),
LockChangeAlert::show),
prefs.getLockCrypt()));
prefs.getLockCrypt())
.nameAndDescription("lockVaultOnHibernation")
.addToggle(prefs.lockVaultOnHibernation)
.hide(prefs.getLockCrypt()
.isNull()
.or(prefs.getLockCrypt().isEmpty())));
return builder.buildComp();
}
}

View file

@ -7,7 +7,9 @@ import io.xpipe.app.util.FixedHierarchyStore;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.*;
import io.xpipe.core.util.UuidHelper;
import javafx.util.Pair;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
@ -311,8 +313,8 @@ public abstract class DataStorage {
DataStoreCategory p = category;
if (share) {
while ((p = DataStorage.get()
.getStoreCategoryIfPresent(p.getParentCategory())
.orElse(null))
.getStoreCategoryIfPresent(p.getParentCategory())
.orElse(null))
!= null) {
p.setShare(true);
}
@ -428,7 +430,8 @@ public abstract class DataStorage {
// Children classes might not be the same, the same goes for state classes
// This can happen when there are multiple child classes and the ids got switched around
if (classMatch) {
DataStore merged = ((FixedChildStore) pair.getKey().getStore()).merge(pair.getValue().getStore().asNeeded());
DataStore merged = ((FixedChildStore) pair.getKey().getStore())
.merge(pair.getValue().getStore().asNeeded());
if (merged != pair.getKey().getStore()) {
pair.getKey().setStoreInternal(merged, false);
}
@ -726,7 +729,7 @@ public abstract class DataStorage {
return children;
}
private List<DataStoreEntry> getHierarchy(DataStoreEntry entry) {
public List<DataStoreEntry> getStoreParentHierarchy(DataStoreEntry entry) {
var es = new ArrayList<DataStoreEntry>();
es.add(entry);
@ -743,7 +746,7 @@ public abstract class DataStorage {
}
public DataStoreId getId(DataStoreEntry entry) {
return DataStoreId.create(getHierarchy(entry).stream()
return DataStoreId.create(getStoreParentHierarchy(entry).stream()
.filter(e -> !(e.getStore() instanceof LocalStore))
.map(e -> e.getName().replaceAll(":", "_"))
.toArray(String[]::new));
@ -781,9 +784,11 @@ public abstract class DataStorage {
public Optional<DataStoreEntry> getStoreEntryIfPresent(@NonNull DataStore store, boolean identityOnly) {
return storeEntriesSet.stream()
.filter(n -> n.getStore() == store
|| (!identityOnly && (n.getStore() != null
&& Objects.equals(store.getClass(), n.getStore().getClass())
&& store.equals(n.getStore()))))
|| (!identityOnly
&& (n.getStore() != null
&& Objects.equals(
store.getClass(), n.getStore().getClass())
&& store.equals(n.getStore()))))
.findFirst();
}

View file

@ -1,17 +1,18 @@
package io.xpipe.app.storage;
import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.FixedHierarchyStore;
import io.xpipe.core.store.*;
import io.xpipe.core.util.JacksonMapper;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.FixedHierarchyStore;
import io.xpipe.core.store.*;
import io.xpipe.core.util.JacksonMapper;
import lombok.*;
import lombok.experimental.NonFinal;
import org.apache.commons.io.FileUtils;

View file

@ -166,7 +166,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
}
@Override
public FailableFunction<LaunchConfiguration, String, Exception> remoteLaunchCommand(ShellDialect systemDialect) {
public FailableFunction<LaunchConfiguration, String, Exception> remoteLaunchCommand(
ShellDialect systemDialect) {
return launchConfiguration -> {
var toExecute = CommandBuilder.of()
.add(executable, "-v", "--title")
@ -549,33 +550,23 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
@Override
public void launch(LaunchConfiguration configuration) throws Exception {
var app = this.getApplicationPath();
if (app.isEmpty()) {
throw new IllegalStateException("iTerm installation not found");
}
try (ShellControl pc = LocalShell.getShell()) {
var a = app.get().toString();
pc.osascriptCommand(String.format(
"""
if application "%s" is not running then
launch application "%s"
if application "iTerm" is not running then
launch application "iTerm"
delay 1
tell application "%s"
tell application "iTerm"
tell current tab of current window
close
end tell
end tell
end if
tell application "%s"
tell application "iTerm"
activate
create window with default profile command "%s"
end tell
""",
a,
a,
a,
a,
configuration.getScriptFile().toString().replaceAll("\"", "\\\\\"")))
.execute();
}
@ -618,7 +609,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
}
@Override
public FailableFunction<LaunchConfiguration, String, Exception> remoteLaunchCommand(ShellDialect systemDialect) {
public FailableFunction<LaunchConfiguration, String, Exception> remoteLaunchCommand(
ShellDialect systemDialect) {
return launchConfiguration -> {
var toExecute = CommandBuilder.of()
.add("open", "-a")
@ -816,7 +808,8 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
}
@Override
public FailableFunction<LaunchConfiguration, String, Exception> remoteLaunchCommand(ShellDialect systemDialect) {
public FailableFunction<LaunchConfiguration, String, Exception> remoteLaunchCommand(
ShellDialect systemDialect) {
return launchConfiguration -> {
var args = toCommand(launchConfiguration);
args.add(0, executable);

View file

@ -1,6 +1,5 @@
package io.xpipe.app.terminal;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.CommandSupport;
import io.xpipe.app.util.LocalShell;
@ -11,6 +10,8 @@ import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.util.XPipeInstallation;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
public interface KittyTerminalType extends ExternalTerminalType {
ExternalTerminalType KITTY_LINUX = new Linux();

View file

@ -36,23 +36,23 @@ public interface TabbyTerminalType extends ExternalTerminalType {
@Override
default TerminalInitFunction additionalInitCommands() {
// return TerminalInitFunction.of(sc -> {
// if (sc.getShellDialect() == ShellDialects.ZSH) {
// return "export PS1=\"$PS1\\[\\e]1337;CurrentDir=\"'$(pwd)\\a\\]'";
// }
// if (sc.getShellDialect() == ShellDialects.BASH) {
// return "precmd () { echo -n \"\\x1b]1337;CurrentDir=$(pwd)\\x07\" }";
// }
// if (sc.getShellDialect() == ShellDialects.FISH) {
// return """
// function __tabby_working_directory_reporting --on-event fish_prompt
// echo -en "\\e]1337;CurrentDir=$PWD\\x7"
// end
// """;
// }
// return null;
// });
return null;
// return TerminalInitFunction.of(sc -> {
// if (sc.getShellDialect() == ShellDialects.ZSH) {
// return "export PS1=\"$PS1\\[\\e]1337;CurrentDir=\"'$(pwd)\\a\\]'";
// }
// if (sc.getShellDialect() == ShellDialects.BASH) {
// return "precmd () { echo -n \"\\x1b]1337;CurrentDir=$(pwd)\\x07\" }";
// }
// if (sc.getShellDialect() == ShellDialects.FISH) {
// return """
// function __tabby_working_directory_reporting --on-event fish_prompt
// echo -en "\\e]1337;CurrentDir=$PWD\\x7"
// end
// """;
// }
// return null;
// });
return TerminalInitFunction.none();
}
class Windows extends ExternalTerminalType.WindowsType implements TabbyTerminalType {

View file

@ -81,9 +81,13 @@ public interface WezTerminalType extends ExternalTerminalType {
@Override
public void launch(LaunchConfiguration configuration) throws Exception {
var path = LocalShell.getShell()
.command(String.format(
"mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications 2>/dev/null",
applicationName))
.readStdoutOrThrow();
var c = CommandBuilder.of()
.addFile(getApplicationPath()
.orElseThrow()
.addFile(Path.of(path)
.resolve("Contents")
.resolve("MacOS")
.resolve("wezterm-gui")

View file

@ -49,7 +49,7 @@ public class UpdateChangelogAlert {
var markdown = new MarkdownComp(update.getRawDescription(), s -> "&nbsp;" + s).createRegion();
alert.getDialogPane().setContent(markdown);
alert.getButtonTypes().add(new ButtonType(AppI18n.get("gotIt"), ButtonBar.ButtonData.OK_DONE));
alert.getButtonTypes().add(new ButtonType(AppI18n.get("ok"), ButtonBar.ButtonData.OK_DONE));
((Stage) alert.getDialogPane().getScene().getWindow()).setAlwaysOnTop(true);
},
r -> r.filter(b -> b.getButtonData().isDefaultButton()).ifPresent(t -> {}));

View file

@ -2,6 +2,7 @@ package io.xpipe.app.util;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.core.util.SecretValue;
import javafx.animation.PauseTransition;
import javafx.scene.input.Clipboard;
import javafx.scene.input.DataFormat;
@ -21,9 +22,16 @@ public class ClipboardHelper {
PlatformThread.runLaterIfNeeded(() -> {
Clipboard clipboard = Clipboard.getSystemClipboard();
Map<DataFormat, Object> previous = Stream.of(DataFormat.PLAIN_TEXT, DataFormat.URL, DataFormat.RTF, DataFormat.HTML, DataFormat.IMAGE, DataFormat.FILES)
.map(dataFormat -> new AbstractMap.SimpleEntry<>(dataFormat, clipboard.getContent(dataFormat))).filter(o -> o.getValue() != null)
.collect(HashMap::new, (m,v)->m.put(v.getKey(), v.getValue()), HashMap::putAll);
Map<DataFormat, Object> previous = Stream.of(
DataFormat.PLAIN_TEXT,
DataFormat.URL,
DataFormat.RTF,
DataFormat.HTML,
DataFormat.IMAGE,
DataFormat.FILES)
.map(dataFormat -> new AbstractMap.SimpleEntry<>(dataFormat, clipboard.getContent(dataFormat)))
.filter(o -> o.getValue() != null)
.collect(HashMap::new, (m, v) -> m.put(v.getKey(), v.getValue()), HashMap::putAll);
var withPassword = new HashMap<>(previous);
withPassword.put(DataFormat.PLAIN_TEXT, pass.getSecretValue());

View file

@ -8,6 +8,7 @@ import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.process.ShellStoreState;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.ShellStore;
import javafx.beans.value.ObservableValue;
import java.util.function.IntFunction;
@ -32,7 +33,8 @@ public class DataStoreFormatter {
return null;
}
if (s.getShellDialect() != null && !s.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) {
if (s.getShellDialect() != null
&& !s.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()) {
if (s.getOsName() != null) {
return formattedOsName(s.getOsName());
}

View file

@ -2,6 +2,8 @@ package io.xpipe.app.util;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.FileKind;
import java.awt.*;
import java.nio.file.Files;
@ -26,7 +28,37 @@ public class DesktopHelper {
return Path.of(System.getProperty("user.home") + "/Desktop");
}
public static void browsePath(Path file) {
public static void browsePathRemote(ShellControl sc, String path, FileKind kind) throws Exception {
var d = sc.getShellDialect();
switch (sc.getOsType()) {
case OsType.Windows windows -> {
// Explorer does not support single quotes, so use normal quotes
if (kind == FileKind.DIRECTORY) {
sc.executeSimpleCommand("explorer " + d.quoteArgument(path));
} else {
sc.executeSimpleCommand("explorer /select," + d.quoteArgument(path));
}
}
case OsType.Linux linux -> {
var action = kind == FileKind.DIRECTORY
? "org.freedesktop.FileManager1.ShowFolders"
: "org.freedesktop.FileManager1.ShowItems";
var dbus = String.format(
"""
dbus-send --session --print-reply --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 %s array:string:"file://%s" string:""
""",
action, path);
sc.executeSimpleCommand(dbus);
}
case OsType.MacOs macOs -> {
sc.executeSimpleCommand("open " + (kind == FileKind.DIRECTORY ? "" : "-R ") + d.fileArgument(path));
}
case OsType.Bsd bsd -> {}
case OsType.Solaris solaris -> {}
}
}
public static void browsePathLocal(Path file) {
if (!Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
return;
}

View file

@ -2,6 +2,7 @@ package io.xpipe.app.util;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
@ -80,4 +81,13 @@ public final class HumanReadableFormat {
private static int getWeekNumber(LocalDateTime date) {
return date.get(WeekFields.of(Locale.getDefault()).weekOfYear());
}
public static String duration(Duration duration) {
return duration.toString()
.substring(2)
.replaceAll("(\\d[HMS])(?!$)", "$1 ")
.replaceAll("\\.\\d+", "")
.toLowerCase();
}
}

View file

@ -71,7 +71,6 @@ public enum PlatformState {
try {
// Weird fix to ensure that macOS quit operation works while in tray.
// Maybe related to https://bugs.openjdk.org/browse/JDK-8318129 as it prints the same error if not called
// The headless check is not needed though but still done
GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();
// Catch more than just the headless exception in case the graphics environment initialization completely
@ -84,9 +83,9 @@ public enum PlatformState {
+ " You don't have to install XPipe on any system like a server, a WSL distribution, a hypervisor, etc.,"
+ " to have full access to that system, a shell connection to it is enough for XPipe to work from your local machine."
: h.getMessage();
TrackEvent.warn(h.getMessage());
TrackEvent.warn(msg);
PlatformState.setCurrent(PlatformState.EXITED);
return Optional.of(ErrorEvent.expected(new HeadlessException(msg)));
return Optional.of(ErrorEvent.expected(new UnsupportedOperationException(msg)));
} catch (Throwable t) {
TrackEvent.warn(t.getMessage());
PlatformState.setCurrent(PlatformState.EXITED);

View file

@ -90,12 +90,12 @@ public class SecretRetrievalStrategyHelper {
new SimpleObjectProperty<>(strat instanceof SecretRetrievalStrategy.CustomCommand i ? i : null);
var map = new LinkedHashMap<String, OptionsBuilder>();
if (allowNone) {
map.put("none", new OptionsBuilder());
map.put("app.none", new OptionsBuilder());
}
map.put("password", inPlace(inPlace));
map.put("passwordManager", passwordManager(passwordManager));
map.put("customCommand", customCommand(customCommand));
map.put("prompt", new OptionsBuilder());
map.put("app.password", inPlace(inPlace));
map.put("app.passwordManager", passwordManager(passwordManager));
map.put("app.customCommand", customCommand(customCommand));
map.put("app.prompt", new OptionsBuilder());
int offset = allowNone ? 0 : -1;
var selected = new SimpleIntegerProperty(

View file

@ -32,7 +32,7 @@ public class TerminalLauncher {
title,
type.shouldClear()
&& AppPrefs.get().clearTerminalOnInit().get(),
null),
TerminalInitFunction.none()),
true);
var config = new ExternalTerminalType.LaunchConfiguration(null, title, title, script, sc.getShellDialect());
type.launch(config);

View file

@ -1,11 +1,12 @@
package io.xpipe.app.util;
import com.sun.jna.platform.win32.Advapi32Util;
import com.sun.jna.platform.win32.WinReg;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.ShellControl;
import com.sun.jna.platform.win32.Advapi32Util;
import com.sun.jna.platform.win32.WinReg;
import java.util.Optional;
public class WindowsRegistry {
@ -56,7 +57,8 @@ public class WindowsRegistry {
}
}
public static Optional<String> findRemoteValuesRecursive(ShellControl shellControl, int hkey, String key, String valueName) throws Exception {
public static Optional<String> findRemoteValuesRecursive(
ShellControl shellControl, int hkey, String key, String valueName) throws Exception {
var command = CommandBuilder.of()
.add("reg", "query")
.addQuoted((hkey == HKEY_LOCAL_MACHINE ? "HKEY_LOCAL_MACHINE" : "HKEY_CURRENT_USER") + "\\" + key)

View file

@ -49,7 +49,6 @@ open module io.xpipe.app {
requires org.slf4j;
requires org.slf4j.jdk.platform.logging;
requires atlantafx.base;
requires org.ocpsoft.prettytime;
requires com.vladsch.flexmark;
requires com.fasterxml.jackson.core;
requires com.fasterxml.jackson.databind;

View file

@ -7,12 +7,13 @@
}
.store-header-bar .menu-button .context-menu {
-fx-padding: 3px;
-fx-background-radius: 4px;
-fx-border-radius: 4px;
-fx-border-color: -color-neutral-muted;
-fx-padding: 0;
-fx-background-radius: 0;
-fx-border-radius: 0;
-fx-border-color: -color-border-default;
}
.context-menu > * > * {
-fx-padding: 3px 10px 3px 10px;
-fx-background-radius: 1px;

View file

@ -94,7 +94,7 @@
/* Section */
.store-entry-section-comp .separator {
.store-entry-section-comp > .separator {
-fx-padding: 0 12px 0 35px;
-fx-border-insets: 0px;
}
@ -103,7 +103,7 @@
-fx-padding: 5px 0 5px 25px;
}
.store-entry-section-comp .separator .line {
.store-entry-section-comp > .separator .line {
-fx-padding: 0;
-fx-border-insets: 0px;
-fx-background-color: -color-border-subtle;
@ -111,7 +111,7 @@
}
.root:pretty .top > .store-entry-section-comp {
-fx-effect: dropshadow(three-pass-box, -color-shadow-default, 2px, 0.5, 0, 1);
-fx-effect: dropshadow(three-pass-box, -color-shadow-default, 2, 0.5, 0, 1);
}
.store-entry-section-comp:root {

View file

@ -2,6 +2,15 @@ import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
import java.util.stream.Stream
buildscript {
configurations.classpath {
resolutionStrategy {
// Fix https://github.com/jreleaser/jreleaser/issues/1643
force 'org.eclipse.jgit:org.eclipse.jgit:5.13.0.202109080827-r'
}
}
}
plugins {
id "io.codearte.nexus-staging" version "0.30.0"
id 'org.gradlex.extra-java-module-info' version '1.8' apply false
@ -85,11 +94,12 @@ project.ext {
changelog = file("dist/changelogs/${canonicalVersionString}.md").exists() ? file("dist/changelogs/${canonicalVersionString}.md").text.trim() + '\n' : ""
productName = isStage ? 'XPipe PTB' : 'XPipe'
kebapProductName = isStage ? 'xpipe-ptb' : 'xpipe'
flatcaseProductName = isStage ? 'xpipeptb' : 'xpipe'
publisher = 'XPipe UG (haftungsbeschränkt)'
shortDescription = 'Your entire server infrastructure at your fingertips'
shortDescription = isStage ? 'XPipe PTB (Public Test Build)' : 'Your entire server infrastructure at your fingertips'
longDescription = 'XPipe is a new type of shell connection hub and remote file manager that allows you to access your entire server infrastructure from your local machine. It works on top of your installed command-line programs that you normally use to connect and does not require any setup on your remote systems.'
website = 'https://xpipe.io'
sourceWebsite = 'https://github.com/xpipe-io/xpipe'
sourceWebsite = isStage ? 'https://github.com/xpipe-io/xpipe-ptb' : 'https://github.com/xpipe-io/xpipe'
authors = 'Christopher Schnick'
javafxVersion = '22.0.1'
platformName = getPlatformName()
@ -107,7 +117,7 @@ project.ext {
"--add-opens", "javafx.graphics/com.sun.javafx.tk.quantum=io.xpipe.app",
"-Xmx8g",
"-Dio.xpipe.app.arch=$rootProject.arch",
"-Dio.xpipe.app.languages=${String.join(";", languages)}",
"-Dio.xpipe.app.languages=${String.join(",", languages)}",
"-Dfile.encoding=UTF-8",
// Disable this for now as it requires Windows 10+
// '-XX:+UseZGC',

View file

@ -1,7 +1,6 @@
package io.xpipe.core.process;
import io.xpipe.core.util.FailableConsumer;
import io.xpipe.core.util.FailableFunction;
import java.io.InputStream;
import java.io.InputStreamReader;
@ -64,11 +63,7 @@ public interface CommandControl extends ProcessControl {
long getExitCode();
default CommandControl elevated(String message) {
return elevated(message, (v) -> true);
}
CommandControl elevated(String message, FailableFunction<ShellControl, Boolean, Exception> elevationFunction);
CommandControl elevated(ElevationFunction function);
void withStdoutOrThrow(FailableConsumer<InputStreamReader, Exception> c);
@ -88,8 +83,12 @@ public interface CommandControl extends ProcessControl {
String readStdoutOrThrow() throws Exception;
String readStderrOrThrow() throws Exception;
String readStdoutAndWait() throws Exception;
String readStderrAndWait() throws Exception;
Optional<String> readStdoutIfPossible() throws Exception;
default boolean discardAndCheckExit() throws ProcessOutputException {

View file

@ -1,9 +0,0 @@
package io.xpipe.core.process;
import lombok.Value;
@Value
public class ElevationConfig {
boolean requiresPassword;
}

View file

@ -0,0 +1,69 @@
package io.xpipe.core.process;
import io.xpipe.core.util.FailableFunction;
public interface ElevationFunction {
static ElevationFunction of(String prefix, FailableFunction<ShellControl, Boolean, Exception> f) {
return new ElevationFunction() {
@Override
public String getPrefix() {
return prefix;
}
@Override
public boolean isSpecified() {
return true;
}
@Override
public boolean apply(ShellControl shellControl) throws Exception {
return f.apply(shellControl);
}
};
}
static ElevationFunction elevated(String prefix) {
return new ElevationFunction() {
@Override
public String getPrefix() {
return prefix;
}
@Override
public boolean isSpecified() {
return true;
}
@Override
public boolean apply(ShellControl shellControl) {
return true;
}
};
}
static ElevationFunction none() {
return new ElevationFunction() {
@Override
public String getPrefix() {
return null;
}
@Override
public boolean isSpecified() {
return false;
}
@Override
public boolean apply(ShellControl shellControl) {
return false;
}
};
}
String getPrefix();
boolean isSpecified();
boolean apply(ShellControl shellControl) throws Exception;
}

View file

@ -171,7 +171,7 @@ public interface ShellControl extends ProcessControl {
OsType.Any getOsType();
ShellControl elevated(String message, FailableFunction<ShellControl, Boolean, Exception> elevationFunction);
ShellControl elevated(ElevationFunction elevationFunction);
ShellControl withInitSnippet(ScriptSnippet snippet);

View file

@ -0,0 +1,10 @@
package io.xpipe.core.store;
public abstract class Session {
public abstract boolean isRunning();
public abstract void start() throws Exception;
public abstract void stop() throws Exception;
}

View file

@ -1,16 +1,6 @@
package io.xpipe.core.store;
public interface SingletonSessionStore<T extends SingletonSessionStore.Session>
extends ExpandedLifecycleStore, InternalCacheDataStore {
abstract class Session {
public abstract boolean isRunning();
public abstract void start() throws Exception;
public abstract void stop() throws Exception;
}
public interface SingletonSessionStore<T extends Session> extends ExpandedLifecycleStore, InternalCacheDataStore {
@Override
default void finalizeValidate() throws Exception {

View file

@ -21,7 +21,8 @@ public interface StatefulDataStore<T extends DataStoreState> extends DataStore {
@SuppressWarnings("unchecked")
default T getState() {
return (T) DataStateProvider.get().getState(this, this::createDefaultState).deepCopy();
return (T)
DataStateProvider.get().getState(this, this::createDefaultState).deepCopy();
}
default void setState(T val) {

View file

@ -1,6 +1,5 @@
package io.xpipe.core.util;
import com.fasterxml.jackson.databind.*;
import io.xpipe.core.dialog.BaseQueryElement;
import io.xpipe.core.dialog.BusyElement;
import io.xpipe.core.dialog.ChoiceElement;
@ -14,6 +13,7 @@ import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import com.fasterxml.jackson.databind.module.SimpleModule;

2
dist/build.gradle vendored
View file

@ -2,7 +2,7 @@
plugins {
id 'org.beryx.jlink' version '3.0.1'
id "org.asciidoctor.jvm.convert" version "4.0.2"
id 'org.jreleaser' version '1.8.0'
id 'org.jreleaser' version '1.11.0'
id("com.netflix.nebula.ospackage") version "11.8.1"
id 'org.gradle.crypto.checksum' version '1.4.0'
id 'signing'

59
dist/changelogs/9.0.1.md vendored Normal file
View file

@ -0,0 +1,59 @@
## Coherent desktops
XPipe now comes with support for remote desktop connections. VNC connections are fully handled over SSH and can therefore be established on top of any existing SSH connection you have in XPipe. RDP support is realized similar to the terminal support, i.e. by launching your preferred RDP client with the connection information. X11-forwarding for SSH is also now supported.
With support for remote graphical desktop connection methods as well now in XPipe 9, the big picture idea is to implement the concept of coherent desktops. Essentially, you can launch predefined desktop applications, terminals, and scripts on any remote desktop connection, regardless of the underlying connection implementation. In combination with the improved SSH tunnel and background session support, you can launch graphical remote applications with one click in the same unified way for VNC over SSH connections, RDP connections, and X11-forwarded SSH connections.
The general implementation and concept will be refined over the next updates.
## SSH connection improvements
- Tunneled and X11-forwarded custom SSH connections are now properly detected and can be toggled on and off to run in the background as normal tunnels. This applies to normal connections and also SSH configs
- The connection establishment has been reworked to reduce the amount of double prompts, e.g. for smartcards or 2FA, where user input is required twice.
- The custom SSH connections now properly apply all configuration options of your user configuration file. They also now correctly apply multiple options for the same key correctly.
- Any value specified for the `RemoteCommand` config option will now be properly applied when launching a terminal. This allows you to still use your preexisting init command setup, e.g. with tmux.
- There is now support defining multiple host entries in place in a custom SSH connection. This is useful for cases where you want to use ProxyJump hosts in place without having to define them elsewhere.
- A host key acceptance notification is now displayed properly in case your system doesn't automatically accept new host keys
## SSH for unknown shells (Professional feature)
There's now an option to not let XPipe interact with the system. In case a system that does not run a known command shell, e.g. a router, link, or some IOT device, XPipe was previously unable to detect the shell type and errored out after some time. This option fixes this problem. This feature is available in the professional edition preview for two weeks.
## SSH X11 Forwarding on Windows via WSL
You can now enable X11 forwarding for an SSH connection.
XPipe allows you to use the WSL2 X11 capabilities on Windows for your SSH connection. The only thing you need for this is a [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) distribution installed on your local system. XPipe it will automatically choose a compatible installed distribution if possible, but you can also use another one in the settings menu.
This means that you don't need to install a separate X11 server on Windows. However, if you are using one anyway, XPipe will detect that and use the currently running X11 server.
## Translations
XPipe 9 now comes with translations for the user interface. These were initially generated with DeepL and can be easily improved and corrected by anyone on GitHub. You can check them out in action and if there is any translation you don't like, submit a quick pull request to fix it. For instructions on how to do this, see https://github.com/xpipe-io/xpipe/tree/master/lang.
## Terminal improvements
The terminal integrations have been reworked across the board. To better show which terminals are well supported and which aren't, there is now a status indicator for every available terminal. This will show you how good the XPipe integration with each one is and which terminals are recommended to be used with XPipe.
The kitty terminal is now fully supported with tabs on both Linux and macOS. The Warp terminal integration now correctly enables all Warp features on remote shells. On macOS, other third-party prompts also now work properly in the launched terminals.
## Password manager improvements
The password manager handling has been improved and some potential sources of errors and confusion have been eliminated. There are also now a few command templates available for established password managers to quickly get started.
## Improved keyboard control
It is a goal to be able to use XPipe only with a keyboard either for productivity or for accessibility reasons. XPipe 9 introduces improved keyboard support with new shortcuts and improved focus control for navigating with the arrow keys, tab, space, and enter.
## Improved logo
The application logo has been improved with of regards to contrast and visibility, which often was a problem on dark backgrounds. It should now stand out on any background color.
## Other changes
There have been countless small bug fixes across the board. They are not listed individually here, but hopefully you will notice some of them.

4
dist/changelogs/9.0.1_incremental.md vendored Normal file
View file

@ -0,0 +1,4 @@
- Fix errors when trying to launch Tabby terminal
- Fix terminal sessions closing instantly after completion
- Fix choco package not being built
- Fix a few broken translation strings

59
dist/changelogs/9.1.md vendored Normal file
View file

@ -0,0 +1,59 @@
## Coherent desktops
XPipe now comes with support for remote desktop connections. VNC connections are fully handled over SSH and can therefore be established on top of any existing SSH connection you have in XPipe. RDP support is realized similar to the terminal support, i.e. by launching your preferred RDP client with the connection information. X11-forwarding for SSH is also now supported.
With support for remote graphical desktop connection methods as well now in XPipe 9, the big picture idea is to implement the concept of coherent desktops. Essentially, you can launch predefined desktop applications, terminals, and scripts on any remote desktop connection, regardless of the underlying connection implementation. In combination with the improved SSH tunnel and background session support, you can launch graphical remote applications with one click in the same unified way for VNC over SSH connections, RDP connections, and X11-forwarded SSH connections.
The general implementation and concept will be refined over the next updates.
## SSH connection improvements
- Tunneled and X11-forwarded custom SSH connections are now properly detected and can be toggled on and off to run in the background as normal tunnels. This applies to normal connections and also SSH configs
- The connection establishment has been reworked to reduce the amount of double prompts, e.g. for smartcards or 2FA, where user input is required twice.
- The custom SSH connections now properly apply all configuration options of your user configuration file. They also now correctly apply multiple options for the same key correctly.
- Any value specified for the `RemoteCommand` config option will now be properly applied when launching a terminal. This allows you to still use your preexisting init command setup, e.g. with tmux.
- There is now support defining multiple host entries in place in a custom SSH connection. This is useful for cases where you want to use ProxyJump hosts in place without having to define them elsewhere.
- A host key acceptance notification is now displayed properly in case your system doesn't automatically accept new host keys
## SSH for unknown shells (Professional feature)
There's now an option to not let XPipe interact with the system. In case a system that does not run a known command shell, e.g. a router, link, or some IOT device, XPipe was previously unable to detect the shell type and errored out after some time. This option fixes this problem. This feature is available in the professional edition preview for two weeks.
## SSH X11 Forwarding on Windows via WSL
You can now enable X11 forwarding for an SSH connection.
XPipe allows you to use the WSL2 X11 capabilities on Windows for your SSH connection. The only thing you need for this is a [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) distribution installed on your local system. XPipe it will automatically choose a compatible installed distribution if possible, but you can also use another one in the settings menu.
This means that you don't need to install a separate X11 server on Windows. However, if you are using one anyway, XPipe will detect that and use the currently running X11 server.
## Translations
XPipe 9 now comes with translations for the user interface. These were initially generated with DeepL and can be easily improved and corrected by anyone on GitHub. You can check them out in action and if there is any translation you don't like, submit a quick pull request to fix it. For instructions on how to do this, see https://github.com/xpipe-io/xpipe/tree/master/lang.
## Terminal improvements
The terminal integrations have been reworked across the board. To better show which terminals are well supported and which aren't, there is now a status indicator for every available terminal. This will show you how good the XPipe integration with each one is and which terminals are recommended to be used with XPipe.
The kitty terminal is now fully supported with tabs on both Linux and macOS. The Warp terminal integration now correctly enables all Warp features on remote shells. On macOS, other third-party prompts also now work properly in the launched terminals.
## Password manager improvements
The password manager handling has been improved and some potential sources of errors and confusion have been eliminated. There are also now a few command templates available for established password managers to quickly get started.
## Improved keyboard control
It is a goal to be able to use XPipe only with a keyboard either for productivity or for accessibility reasons. XPipe 9 introduces improved keyboard support with new shortcuts and improved focus control for navigating with the arrow keys, tab, space, and enter.
## Improved logo
The application logo has been improved with of regards to contrast and visibility, which often was a problem on dark backgrounds. It should now stand out on any background color.
## Other changes
There have been countless small bug fixes across the board. They are not listed individually here, but hopefully you will notice some of them.

14
dist/changelogs/9.1_incremental.md vendored Normal file
View file

@ -0,0 +1,14 @@
- Add ability to drop files into an active VNC session to transfer them to the system
- Rework VNC connections to distinguish between VNC server host and actual target system host for cases like VMs where the server host and controlled target system might be different
- Add new setting to automatically lock vault when the local system goes into hibernation/sleep mode and a custom vault passphrase was set
- Fix sudo elevation password not being filled automatically when launching some remote connection in a terminal
- Fix git sometimes complaining about an unknown author identity when cloning on new systems
- Fix search for connections dialog sometimes throwing errors
- Fix NullPointer when launching an SFTP Client/Termius/VSCode for an SSH connection without a password set
- Fix macOS terminal and editor app recognition to be more accurate
- Fix file browser overview buttons both opening the same directory
- Fix exception on Linux when desktop directory did not exist
- Fix out of bounds error for certain VNC key input
- Fix NullPointers when launching a desktop environment for an X11 SSH connection
- Fix some NullPointers in the file browser
- Fix some styling issues

59
dist/changelogs/9.2.md vendored Normal file
View file

@ -0,0 +1,59 @@
## Coherent desktops
XPipe now comes with support for remote desktop connections. VNC connections are fully handled over SSH and can therefore be established on top of any existing SSH connection you have in XPipe. RDP support is realized similar to the terminal support, i.e. by launching your preferred RDP client with the connection information. X11-forwarding for SSH is also now supported.
With support for remote graphical desktop connection methods as well now in XPipe 9, the big picture idea is to implement the concept of coherent desktops. Essentially, you can launch predefined desktop applications, terminals, and scripts on any remote desktop connection, regardless of the underlying connection implementation. In combination with the improved SSH tunnel and background session support, you can launch graphical remote applications with one click in the same unified way for VNC over SSH connections, RDP connections, and X11-forwarded SSH connections.
The general implementation and concept will be refined over the next updates.
## SSH connection improvements
- Tunneled and X11-forwarded custom SSH connections are now properly detected and can be toggled on and off to run in the background as normal tunnels. This applies to normal connections and also SSH configs
- The connection establishment has been reworked to reduce the amount of double prompts, e.g. for smartcards or 2FA, where user input is required twice.
- The custom SSH connections now properly apply all configuration options of your user configuration file. They also now correctly apply multiple options for the same key correctly.
- Any value specified for the `RemoteCommand` config option will now be properly applied when launching a terminal. This allows you to still use your preexisting init command setup, e.g. with tmux.
- There is now support defining multiple host entries in place in a custom SSH connection. This is useful for cases where you want to use ProxyJump hosts in place without having to define them elsewhere.
- A host key acceptance notification is now displayed properly in case your system doesn't automatically accept new host keys
## SSH for unknown shells (Professional feature)
There's now an option to not let XPipe interact with the system. In case a system that does not run a known command shell, e.g. a router, link, or some IOT device, XPipe was previously unable to detect the shell type and errored out after some time. This option fixes this problem. This feature is available in the professional edition preview for two weeks.
## SSH X11 Forwarding on Windows via WSL
You can now enable X11 forwarding for an SSH connection.
XPipe allows you to use the WSL2 X11 capabilities on Windows for your SSH connection. The only thing you need for this is a [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) distribution installed on your local system. XPipe it will automatically choose a compatible installed distribution if possible, but you can also use another one in the settings menu.
This means that you don't need to install a separate X11 server on Windows. However, if you are using one anyway, XPipe will detect that and use the currently running X11 server.
## Translations
XPipe 9 now comes with translations for the user interface. These were initially generated with DeepL and can be easily improved and corrected by anyone on GitHub. You can check them out in action and if there is any translation you don't like, submit a quick pull request to fix it. For instructions on how to do this, see https://github.com/xpipe-io/xpipe/tree/master/lang.
## Terminal improvements
The terminal integrations have been reworked across the board. To better show which terminals are well supported and which aren't, there is now a status indicator for every available terminal. This will show you how good the XPipe integration with each one is and which terminals are recommended to be used with XPipe.
The kitty terminal is now fully supported with tabs on both Linux and macOS. The Warp terminal integration now correctly enables all Warp features on remote shells. On macOS, other third-party prompts also now work properly in the launched terminals.
## Password manager improvements
The password manager handling has been improved and some potential sources of errors and confusion have been eliminated. There are also now a few command templates available for established password managers to quickly get started.
## Improved keyboard control
It is a goal to be able to use XPipe only with a keyboard either for productivity or for accessibility reasons. XPipe 9 introduces improved keyboard support with new shortcuts and improved focus control for navigating with the arrow keys, tab, space, and enter.
## Improved logo
The application logo has been improved with of regards to contrast and visibility, which often was a problem on dark backgrounds. It should now stand out on any background color.
## Other changes
There have been countless small bug fixes across the board. They are not listed individually here, but hopefully you will notice some of them.

1
dist/changelogs/9.2_incremental.md vendored Normal file
View file

@ -0,0 +1 @@
- Fix offline licenses not properly applying

View file

@ -1,2 +1,3 @@
-javaagent:$HOME/.attachme/attachme-agent-1.2.4.jar=port:7857,host:localhost
-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0
-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0
-XX:+EnableDynamicAgentLoading

View file

@ -1,2 +1,3 @@
-javaagent:$HOME/.attachme/attachme-agent-1.2.4.jar=port:7857,host:localhost
-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0
-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0
-XX:+EnableDynamicAgentLoading

View file

@ -1,2 +1,3 @@
-javaagent:%userprofile%\\.attachme\\attachme-agent-1.2.4.jar=port:7857,host:localhost
-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0
-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0
-XX:+EnableDynamicAgentLoading

View file

@ -8,8 +8,10 @@ import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.process.ShellStoreState;
import io.xpipe.core.store.ShellStore;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import lombok.Value;
public class BrowseStoreAction implements ActionProvider {
@ -22,8 +24,8 @@ public class BrowseStoreAction implements ActionProvider {
public boolean isApplicable(DataStoreEntryRef<ShellStore> o) {
var state = o.get().getStorePersistentState();
if (state instanceof ShellStoreState shellStoreState) {
return shellStoreState.getShellDialect() == null ||
shellStoreState.getShellDialect().getDumbMode().supportsAnyPossibleInteraction();
return shellStoreState.getShellDialect() == null
|| shellStoreState.getShellDialect().getDumbMode().supportsAnyPossibleInteraction();
} else {
return true;
}

View file

@ -43,8 +43,9 @@ public class LaunchAction implements ActionProvider {
@Override
public boolean isApplicable(DataStoreEntryRef<DataStore> o) {
return o.get().getValidity().isUsable() && (o.getStore() instanceof LaunchableStore || o.get().getProvider().launchAction(o.get()) !=
null);
return o.get().getValidity().isUsable()
&& (o.getStore() instanceof LaunchableStore
|| o.get().getProvider().launchAction(o.get()) != null);
}
@Override
@ -75,8 +76,9 @@ public class LaunchAction implements ActionProvider {
@Override
public boolean isApplicable(DataStoreEntryRef<DataStore> o) {
return o.get().getValidity().isUsable() && (o.getStore() instanceof LaunchableStore || o.get().getProvider().launchAction(o.get()) !=
null);
return o.get().getValidity().isUsable()
&& (o.getStore() instanceof LaunchableStore
|| o.get().getProvider().launchAction(o.get()) != null);
}
};
}

View file

@ -5,6 +5,7 @@ import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.process.CommandControl;
import io.xpipe.core.process.ElevationFunction;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.store.LocalStore;
@ -125,7 +126,7 @@ public class SampleAction implements ActionProvider {
// by using the information from the connection store.
// You can also set a custom working directory.
try (CommandControl cc = sc.command("kill <pid>")
.elevated("kill")
.elevated(ElevationFunction.elevated("kill"))
.withWorkingDirectory("/")
.start()) {
// Discard any output but throw an exception with the stderr contents if the exit code is not 0

View file

@ -7,7 +7,9 @@ import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.ScanAlert;
import io.xpipe.core.process.ShellStoreState;
import io.xpipe.core.store.ShellStore;
import javafx.beans.value.ObservableValue;
import lombok.Value;
public class ScanAction implements ActionProvider {
@ -39,8 +41,8 @@ public class ScanAction implements ActionProvider {
var state = o.get().getStorePersistentState();
if (state instanceof ShellStoreState shellStoreState) {
return shellStoreState.getShellDialect() == null ||
shellStoreState.getShellDialect().getDumbMode().supportsAnyPossibleInteraction();
return shellStoreState.getShellDialect() == null
|| shellStoreState.getShellDialect().getDumbMode().supportsAnyPossibleInteraction();
} else {
return true;
}

View file

@ -4,11 +4,10 @@ import io.xpipe.app.browser.action.LeafAction;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.DesktopHelper;
import io.xpipe.app.util.LocalShell;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialect;
import io.xpipe.core.store.FileKind;
import javafx.beans.value.ObservableValue;
@ -19,34 +18,12 @@ public class BrowseInNativeManagerAction implements LeafAction {
@Override
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) throws Exception {
ShellControl sc = model.getFileSystem().getShell().orElseThrow();
ShellDialect d = sc.getShellDialect();
for (BrowserEntry entry : entries) {
var e = entry.getRawFileEntry().getPath();
var localFile = sc.getLocalSystemAccess().translateToLocalSystemPath(e);
try (var local = LocalShell.getShell().start()) {
switch (OsType.getLocal()) {
case OsType.Windows windows -> {
// Explorer does not support single quotes, so use normal quotes
if (entry.getRawFileEntry().getKind() == FileKind.DIRECTORY) {
local.executeSimpleCommand("explorer " + d.quoteArgument(localFile));
} else {
local.executeSimpleCommand("explorer /select," + d.quoteArgument(localFile));
}
}
case OsType.Linux linux -> {
var action = entry.getRawFileEntry().getKind() == FileKind.DIRECTORY ?
"org.freedesktop.FileManager1.ShowFolders" :
"org.freedesktop.FileManager1.ShowItems";
var dbus = String.format("""
dbus-send --session --print-reply --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 %s array:string:"file://%s" string:""
""", action, localFile);
local.executeSimpleCommand(dbus);
}
case OsType.MacOs macOs -> {
local.executeSimpleCommand(
"open " + (entry.getRawFileEntry().getKind() == FileKind.DIRECTORY ? "" : "-R ") + d.fileArgument(localFile));
}
}
DesktopHelper.browsePathRemote(
local, localFile, entry.getRawFileEntry().getKind());
}
}
}

View file

@ -22,7 +22,7 @@ public class CopyAction implements LeafAction {
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) {
BrowserClipboard.startCopy(
model.getCurrentDirectory(),
entries.stream().map(entry -> entry.getRawFileEntry()).toList());
entries);
}
@Override

View file

@ -37,7 +37,8 @@ public class EditFileAction implements LeafAction {
@Override
public ObservableValue<String> getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
var e = AppPrefs.get().externalEditor().getValue();
return AppI18n.observable("editWithEditor", e.toTranslatedString().getValue());
return AppI18n.observable(
"editWithEditor", e != null ? e.toTranslatedString().getValue() : "?");
}
@Override

View file

@ -9,6 +9,7 @@ import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.ShellControl;
import javafx.beans.value.ObservableValue;
import java.util.List;

View file

@ -34,7 +34,7 @@ public class PasteAction implements LeafAction {
return;
}
model.dropFilesIntoAsync(target, files, true);
model.dropFilesIntoAsync(target, files.stream().map(browserEntry -> browserEntry.getRawFileEntry()).toList(), true);
}
@Override

View file

@ -74,7 +74,8 @@ public class DesktopCommandStoreProvider implements DataStoreProvider {
entry,
env,
DesktopEnvironmentStore.class,
desktopStoreDataStoreEntryRef -> desktopStoreDataStoreEntryRef.getStore().supportsDesktopAccess(),
desktopStoreDataStoreEntryRef ->
desktopStoreDataStoreEntryRef.getStore().supportsDesktopAccess(),
StoreViewState.get().getAllConnectionsCategory()),
env)
.nonNull()

View file

@ -62,7 +62,7 @@ public class DesktopEnvironmentStore extends JacksonizedValue
initCommands.add(command);
}
var joined = String.join(dialect.getNewLine().getNewLineString(), initCommands);
return !joined.isBlank() ? joined : null;
return joined;
}
@Override
@ -95,9 +95,9 @@ public class DesktopEnvironmentStore extends JacksonizedValue
: getMergedInitCommands(null));
var scriptFile = base.getStore().createScript(dialect, toExecute);
var launchScriptFile = base.getStore()
.createScript(dialect, dialect.prepareTerminalInitFileOpenCommand(dialect, null, scriptFile.toString()));
var launchConfig =
new ExternalTerminalType.LaunchConfiguration(null, name, name, launchScriptFile, dialect);
.createScript(
dialect, dialect.prepareTerminalInitFileOpenCommand(dialect, null, scriptFile.toString()));
var launchConfig = new ExternalTerminalType.LaunchConfiguration(null, name, name, launchScriptFile, dialect);
base.getStore().runDesktopScript(name, launchCommand.apply(launchConfig));
}

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