Many small fixes

This commit is contained in:
crschnick 2023-02-21 23:41:39 +00:00
parent 27d25ca666
commit fab1d75f45
22 changed files with 269 additions and 198 deletions

View file

@ -73,7 +73,7 @@ public class BrowserComp extends SimpleComp {
}
model.getOpenFileSystems().addListener((ListChangeListener<? super OpenFileSystemModel>) c -> {
PlatformThread.runLaterBlocking(() -> {
PlatformThread.runLaterIfNeededBlocking(() -> {
while (c.next()) {
for (var r : c.getRemoved()) {
var t = map.remove(r);

View file

@ -2,26 +2,51 @@
package io.xpipe.app.browser;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.ExternalEditor;
import io.xpipe.app.util.TerminalHelper;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellProcessControl;
import io.xpipe.core.store.FileSystem;
import javafx.beans.property.Property;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import org.apache.commons.io.FilenameUtils;
import java.util.List;
final class FileContextMenu extends ContextMenu {
public boolean isScript(FileSystem.FileEntry e) {
if (e.isDirectory()) {
return false;
}
var shell = e.getFileSystem().getShell();
if (shell.isEmpty()) {
return false;
}
var os = shell.get().getOsType();
var ending = FilenameUtils.getExtension(e.getPath()).toLowerCase();
if (os.equals(OsType.WINDOWS) && List.of("bat", "ps1", "cmd").contains(ending)) {
return true;
}
return false;
}
private final OpenFileSystemModel model;
private final String path;
private final boolean directory;
private final FileSystem.FileEntry entry;
private final Property<String> editing;
public FileContextMenu(OpenFileSystemModel model, String path, boolean directory, Property<String> editing) {
public FileContextMenu(OpenFileSystemModel model, FileSystem.FileEntry entry, Property<String> editing) {
super();
this.model = model;
this.path = path;
this.directory = directory;
this.entry = entry;
this.editing = editing;
createMenu();
}
@ -30,14 +55,14 @@ final class FileContextMenu extends ContextMenu {
var cut = new MenuItem("Delete");
cut.setOnAction(event -> {
event.consume();
model.deleteAsync(path);
model.deleteAsync(entry.getPath());
});
cut.setAccelerator(new KeyCodeCombination(KeyCode.DELETE));
var rename = new MenuItem("Rename");
rename.setOnAction(event -> {
event.consume();
editing.setValue(path);
editing.setValue(entry.getPath());
});
rename.setAccelerator(new KeyCodeCombination(KeyCode.F2));
@ -47,20 +72,50 @@ final class FileContextMenu extends ContextMenu {
rename
);
if (directory) {
if (entry.isDirectory()) {
var terminal = new MenuItem("Terminal");
terminal.setOnAction(event -> {
event.consume();
model.openTerminalAsync(path);
model.openTerminalAsync(entry.getPath());
});
getItems().add(0, terminal);
} else {
var open = new MenuItem("Edit");
var open = new MenuItem("Open");
open.setOnAction(event -> {
event.consume();
ExternalEditor.get().openInEditor(model.getFileSystem(), path);
ExternalEditor.get().openInEditor(model.getFileSystem(), entry.getPath());
});
getItems().add(0, open);
if (isScript(entry)) {
var executeInBackground = new MenuItem("Run in background");
executeInBackground.setOnAction(event -> {
event.consume();
ExternalEditor.get().openInEditor(model.getFileSystem(), entry.getPath());
});
getItems().add(0, executeInBackground);
var execute = new MenuItem("Run in terminal");
execute.setOnAction(event -> {
event.consume();
try {
ShellProcessControl pc = model.getFileSystem().getShell().orElseThrow();
pc.executeSimpleCommand(pc.getShellDialect().getMakeExecutableCommand(entry.getPath()));
var cmd = pc.command(entry.getPath()).prepareTerminalOpen();
TerminalHelper.open(FilenameUtils.getName(entry.getPath()), cmd);
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
});
getItems().add(0, execute);
}
var edit = new MenuItem("Edit");
edit.setOnAction(event -> {
event.consume();
ExternalEditor.get().openInEditor(model.getFileSystem(), entry.getPath());
});
getItems().add(0, edit);
}
}
}

View file

@ -8,6 +8,7 @@ import io.xpipe.app.comp.base.LazyTextFieldComp;
import io.xpipe.app.core.AppResources;
import io.xpipe.app.fxcomps.impl.PrettyImageComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.util.Containers;
import io.xpipe.app.util.HumanReadableFormat;
import io.xpipe.core.impl.FileNames;
@ -47,7 +48,9 @@ final class FileListComp extends AnchorPane {
public FileListComp(FileListModel fileList) {
this.fileList = fileList;
TableView<FileSystem.FileEntry> table = createTable();
fileList.getComparatorProperty().bind(table.comparatorProperty());
SimpleChangeListener.apply(table.comparatorProperty(), (newValue) -> {
fileList.setComparator(newValue);
});
getChildren().setAll(table);
getStyleClass().addAll("table-directory-view");
@ -118,8 +121,7 @@ final class FileListComp extends AnchorPane {
var cm = new FileContextMenu(
fileList.getModel(),
row.getItem().getPath(),
row.getItem().isDirectory(),
row.getItem(),
editing);
if (t.getButton() == MouseButton.SECONDARY) {
cm.show(row, t.getScreenX(), t.getScreenY());
@ -231,7 +233,10 @@ final class FileListComp extends AnchorPane {
return row;
});
BindingsHelper.bindContent(table.getItems(), fileList.getShown());
fileList.getShown().addListener((observable, oldValue, newValue) -> {
BindingsHelper.setContent(table.getItems(), newValue);
});
return table;
}

View file

@ -6,17 +6,14 @@ import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.ExternalEditor;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.store.FileSystem;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import lombok.Getter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.Predicate;
@Getter
@ -30,28 +27,33 @@ final class FileListModel {
private final OpenFileSystemModel model;
private final Property<Comparator<FileSystem.FileEntry>> comparatorProperty =
new SimpleObjectProperty<>(FILE_TYPE_COMPARATOR);
private final ObservableList<FileSystem.FileEntry> all = FXCollections.observableArrayList();
private final ObservableList<FileSystem.FileEntry> shown;
private final Property<List<FileSystem.FileEntry>> all = new SimpleObjectProperty<>(List.of());
private final Property<List<FileSystem.FileEntry>> shown = new SimpleObjectProperty<>(List.of());
private final ObjectProperty<Predicate<FileSystem.FileEntry>> predicateProperty =
new SimpleObjectProperty<>(path -> true);
public FileListModel(OpenFileSystemModel model) {
this.model = model;
var filteredList = new FilteredList<>(all);
filteredList.predicateProperty().bind(predicateProperty);
}
var sortedList = new SortedList<>(filteredList);
sortedList
.comparatorProperty()
.bind(Bindings.createObjectBinding(
() -> {
Comparator<FileSystem.FileEntry> tableComparator = comparatorProperty.getValue();
return tableComparator != null
? FILE_TYPE_COMPARATOR.thenComparing(tableComparator)
: FILE_TYPE_COMPARATOR;
},
comparatorProperty));
shown = sortedList;
public void setAll(List<FileSystem.FileEntry> newFiles) {
all.setValue(newFiles);
refreshShown();
}
public void setComparator(Comparator<FileSystem.FileEntry> comparator) {
comparatorProperty.setValue(comparator);
refreshShown();
}
private void refreshShown() {
Comparator<FileSystem.FileEntry> tableComparator = comparatorProperty.getValue();
var comparator = tableComparator != null
? FILE_TYPE_COMPARATOR.thenComparing(tableComparator)
: FILE_TYPE_COMPARATOR;
var listCopy = new ArrayList<>(all.getValue());
listCopy.sort(comparator);
shown.setValue(listCopy);
}
public boolean rename(String filename, String newName) {

View file

@ -2,6 +2,7 @@ package io.xpipe.app.browser;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.impl.FileNames;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileSystem;
import io.xpipe.core.store.ShellStore;
@ -14,10 +15,32 @@ public class FileSystemHelper {
private static OpenFileSystemModel local;
public static String normalizeDirectoryPath(OpenFileSystemModel model, String path) {
if (path == null) {
return null;
}
path = path.trim();
if (path.isBlank()) {
return null;
}
var shell = model.getFileSystem().getShell();
if (shell.isEmpty()) {
return path;
}
if (shell.get().getOsType().equals(OsType.WINDOWS) && path.length() == 2 && path.endsWith(":")) {
return path + "\\";
}
return FileNames.toDirectory(path);
}
public static OpenFileSystemModel getLocal() throws Exception {
if (local == null) {
var model = new OpenFileSystemModel();
model.switchSync(ShellStore.local());
model.switchFileSystem(ShellStore.local());
local = model;
}

View file

@ -25,14 +25,19 @@ final class NavigationHistory {
return history.size() > 0 ? history.get(cursor.get()) : null;
}
public void append(String s) {
public void cd(String s) {
if (s == null) {
return;
}
var lastString = history.size() > 0 ? history.get(history.size() - 1) : null;
if (!Objects.equals(lastString, s)) {
history.add(s);
var lastString = getCurrent();
if (Objects.equals(lastString, s)) {
return;
}
if (canGoForth.get()) {
history.subList(cursor.get() + 1, history.size()).clear();
}
history.add(s);
cursor.set(history.size() - 1);
}

View file

@ -2,7 +2,6 @@
package io.xpipe.app.browser;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.BusyProperty;
import io.xpipe.app.util.TerminalHelper;
@ -17,15 +16,15 @@ import lombok.Getter;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@Getter
final class OpenFileSystemModel {
private Property<FileSystemStore> store = new SimpleObjectProperty<>();
private FileSystem fileSystem;
private List<String> roots;
private final FileListModel fileList;
private final ReadOnlyObjectWrapper<String> currentPath = new ReadOnlyObjectWrapper<>();
private final NavigationHistory history = new NavigationHistory();
@ -51,35 +50,35 @@ final class OpenFileSystemModel {
public void cd(String path) {
ThreadHelper.runFailableAsync(() -> {
cdSync(path);
try (var ignored = new BusyProperty(busy)) {
cdSync(path);
}
});
}
private boolean cdSync(String path) {
try (var ignored = new BusyProperty(busy)) {
if (!navigateTo(path)) {
return false;
}
path = FileSystemHelper.normalizeDirectoryPath(this, path);
currentPath.set(path);
if (!Objects.equals(history.getCurrent(), path)) {
history.append(path);
}
return true;
if (!navigateToSync(path)) {
return false;
}
currentPath.set(path);
history.cd(path);
return true;
}
private boolean navigateTo(String dir) {
private boolean navigateToSync(String dir) {
try {
List<FileSystem.FileEntry> newList;
if (dir != null) {
newList = getFileSystem().listFiles(dir).toList();
newList = getFileSystem().listFiles(dir).collect(Collectors.toCollection(ArrayList::new));
} else {
newList = getFileSystem().listRoots().stream()
.map(s -> new FileSystem.FileEntry(getFileSystem(), s, Instant.now(), true, false, 0))
.toList();
.collect(Collectors.toCollection(ArrayList::new));
}
BindingsHelper.setContent(fileList.getAll(), newList);
fileList.setAll(newList);
return true;
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
@ -96,7 +95,8 @@ final class OpenFileSystemModel {
});
}
public void dropFilesIntoAsync(FileSystem.FileEntry target, List<FileSystem.FileEntry> files, boolean explicitCopy) {
public void dropFilesIntoAsync(
FileSystem.FileEntry target, List<FileSystem.FileEntry> files, boolean explicitCopy) {
ThreadHelper.runFailableAsync(() -> {
BusyProperty.execute(busy, () -> {
FileSystemHelper.dropFilesInto(target, files, explicitCopy);
@ -154,26 +154,30 @@ final class OpenFileSystemModel {
store = null;
}
public void switchSync(FileSystemStore fileSystem) throws Exception {
public void switchFileSystem(FileSystemStore fileSystem) throws Exception {
BusyProperty.execute(busy, () -> {
closeSync();
this.store.setValue(fileSystem);
var fs = fileSystem.createFileSystem();
fs.open();
this.fileSystem = fs;
var current = fs instanceof ConnectionFileSystem connectionFileSystem
? connectionFileSystem
.getShellProcessControl()
.executeStringSimpleCommand(connectionFileSystem
.getShellProcessControl()
.getShellDialect()
.getPrintWorkingDirectoryCommand())
: null;
cdSync(current);
switchSync(fileSystem);
});
}
private void switchSync(FileSystemStore fileSystem) throws Exception {
closeSync();
this.store.setValue(fileSystem);
var fs = fileSystem.createFileSystem();
fs.open();
this.fileSystem = fs;
var current = fs instanceof ConnectionFileSystem connectionFileSystem
? connectionFileSystem
.getShellProcessControl()
.executeStringSimpleCommand(connectionFileSystem
.getShellProcessControl()
.getShellDialect()
.getPrintWorkingDirectoryCommand())
: null;
cdSync(current);
}
public void switchAsync(FileSystemStore fileSystem) {
ThreadHelper.runFailableAsync(() -> {
switchSync(fileSystem);

View file

@ -1,57 +0,0 @@
/* SPDX-License-Identifier: MIT */
package io.xpipe.app.browser;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
final class Utils {
private Utils() {
// Default constructor
}
public static long fileSize(Path path) {
if (path == null) {
return 0;
}
try {
return Files.size(path);
} catch (IOException e) {
return 0;
}
}
public static boolean isFileHidden(Path path) {
if (path == null) {
return false;
}
try {
return Files.isHidden(path);
} catch (IOException e) {
return false;
}
}
public static FileTime fileMTime(Path path, LinkOption... options) {
if (path == null) {
return null;
}
try {
return Files.getLastModifiedTime(path, options);
} catch (IOException e) {
return null;
}
}
public static String getMimeType(Path path) {
try {
return Files.probeContentType(path);
} catch (IOException e) {
return null;
}
}
}

View file

@ -108,13 +108,13 @@ public class GuiDsStoreSelectStep extends MultiStepComp.Step<CompStructure<? ext
return;
}
PlatformThread.runLaterBlocking(() -> {
PlatformThread.runLaterIfNeededBlocking(() -> {
baseSource.setValue(ds.asNeeded());
parent.next();
});
} catch (Exception e) {
ErrorEvent.fromThrowable(e).build().handle();
PlatformThread.runLaterBlocking(() -> {
PlatformThread.runLaterIfNeededBlocking(() -> {
baseSource.setValue(null);
});
}

View file

@ -273,7 +273,7 @@ public class PlatformThread {
}
}
public static void runLaterBlocking(Runnable r) {
public static void runLaterIfNeededBlocking(Runnable r) {
if (!Platform.isFxApplicationThread()) {
CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(() -> {
@ -288,4 +288,16 @@ public class PlatformThread {
r.run();
}
}
public static void alwaysRunLaterBlocking(Runnable r) {
CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(() -> {
r.run();
latch.countDown();
});
try {
latch.await();
} catch (InterruptedException ignored) {
}
}
}

View file

@ -37,7 +37,10 @@ public class ErrorHandlerComp extends SimpleComp {
}
public static void showAndWait(ErrorEvent event) {
PlatformThread.runLaterBlocking(() -> {
// Always run later to prevent any issues when an exception
// is thrown within an animation or layout processing task
// Otherwise, the show and wait method might fail
PlatformThread.alwaysRunLaterBlocking(() -> {
synchronized (showing) {
if (!showing.get()) {
showing.set(true);

View file

@ -15,7 +15,7 @@ public class UpdateChangelogAlert {
public static void showIfNeeded() {
var update = AppUpdater.get().getPerformedUpdate();
if (update != null && !update.getNewVersion().equals(AppProperties.get().getVersion())) {
if (update != null && !AppProperties.get().getVersion().equals(update.getNewVersion())) {
ErrorEvent.fromMessage("Update did not succeed").handle();
return;
}

View file

@ -15,6 +15,10 @@ public class BusyProperty implements AutoCloseable {
public BusyProperty(BooleanProperty prop) {
this.prop = prop;
while (prop.get()) {
ThreadHelper.sleep(50);
}
prop.setValue(true);
}

View file

@ -1,7 +1,7 @@
.bar {
-fx-padding: 0.8em 1.0em 0.8em 1.0em;
-fx-background-color: -color-neutral-muted;
-fx-border-color: -color-neutral-emphasis;
-fx-background-color: -color-neutral-subtle;
-fx-border-color: -color-border-default;
}
.store-header-bar {

View file

@ -5,6 +5,18 @@ import java.util.List;
public class FileNames {
public static String toDirectory(String path) {
if (path.endsWith("/") || path.endsWith("\\")) {
return path;
}
if (path.contains("\\")) {
return path + "\\";
}
return path + "/";
}
public static String getFileName(String file) {
var split = file.split("[\\\\/]");
if (split.length == 0) {

View file

@ -12,6 +12,7 @@ import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
@JsonTypeName("local")
@ -26,6 +27,11 @@ public class LocalStore extends JacksonizedValue implements FileSystemStore, Mac
public FileSystem createFileSystem() {
if (true) return new ConnectionFileSystem(ShellStore.local().create());
return new FileSystem() {
@Override
public Optional<ShellProcessControl> getShell() {
return Optional.empty();
}
@Override
public FileSystem open() throws Exception {
return this;

View file

@ -5,7 +5,6 @@ import lombok.SneakyThrows;
import java.io.*;
import java.nio.charset.Charset;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
public interface CommandProcessControl extends ProcessControl {
@ -22,47 +21,29 @@ public interface CommandProcessControl extends ProcessControl {
ShellProcessControl getParent();
default InputStream startExternalStdout() throws Exception {
try {
start();
AtomicReference<String> err = new AtomicReference<>("");
accumulateStderr(s -> err.set(s));
return new FilterInputStream(getStdout()) {
@Override
@SneakyThrows
public void close() throws IOException {
CommandProcessControl.this.close();
if (!err.get().isEmpty()) {
throw new IOException(err.get());
}
CommandProcessControl.this.getParent().restart();
}
};
} catch (Exception ex) {
close();
throw ex;
}
start();
discardErr();
return new FilterInputStream(getStdout()) {
@Override
@SneakyThrows
public void close() throws IOException {
CommandProcessControl.this.close();
}
};
}
default OutputStream startExternalStdin() throws Exception {
try {
start();
discardOut();
discardErr();
return new FilterOutputStream(getStdin()) {
@Override
@SneakyThrows
public void close() throws IOException {
closeStdin();
CommandProcessControl.this.close();
CommandProcessControl.this.getParent().restart();
}
};
} catch (Exception ex) {
close();
throw ex;
}
start();
discardOut();
discardErr();
return new FilterOutputStream(getStdin()) {
@Override
@SneakyThrows
public void close() throws IOException {
closeStdin();
CommandProcessControl.this.close();
}
};
}
public boolean waitFor();

View file

@ -1,24 +1,28 @@
package io.xpipe.core.process;
import lombok.Getter;
@Getter
public class ProcessOutputException extends Exception {
public ProcessOutputException() {
super();
public static ProcessOutputException of(String customPrefix, ProcessOutputException ex) {
var messageSuffix = ex.getOutput() != null && ! ex.getOutput().isBlank()?": " + ex.getOutput() : "";
var message = customPrefix + messageSuffix;
return new ProcessOutputException(message, ex.getExitCode(), ex.getOutput());
}
public ProcessOutputException(String message) {
public static ProcessOutputException of(int exitCode, String output) {
var messageSuffix = output != null && !output.isBlank()?": " + output : "";
var message = exitCode == -1 ? "Process timed out" + messageSuffix : "Process returned with exit code " + exitCode + messageSuffix;
return new ProcessOutputException(message, exitCode, output);
}
private final int exitCode;
private final String output;
private ProcessOutputException(String message, int exitCode, String output) {
super(message);
}
public ProcessOutputException(String message, Throwable cause) {
super(message, cause);
}
public ProcessOutputException(Throwable cause) {
super(cause);
}
protected ProcessOutputException(
String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
this.exitCode = exitCode;
this.output = output;
}
}

View file

@ -7,11 +7,14 @@ import lombok.NonNull;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.Semaphore;
import java.util.function.Consumer;
import java.util.function.Predicate;
public interface ShellProcessControl extends ProcessControl {
Semaphore getCommandLock();
void onInit(Consumer<ShellProcessControl> pc);
String prepareTerminalOpen() throws Exception;
@ -44,8 +47,7 @@ public interface ShellProcessControl extends ProcessControl {
try (CommandProcessControl c = command(command).start()) {
c.discardOrThrow();
} catch (ProcessOutputException out) {
var message = out.getMessage();
throw new ProcessOutputException(message != null ? failMessage + ": " + message : failMessage);
throw ProcessOutputException.of(failMessage, out);
}
}

View file

@ -8,6 +8,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
@Getter
@ -33,6 +34,11 @@ public class ConnectionFileSystem implements FileSystem {
return shellProcessControl.getShellDialect().listFiles(this, shellProcessControl, file);
}
@Override
public Optional<ShellProcessControl> getShell() {
return Optional.of(shellProcessControl);
}
@Override
public FileSystem open() throws Exception {
shellProcessControl.start();

View file

@ -1,5 +1,6 @@
package io.xpipe.core.store;
import io.xpipe.core.process.ShellProcessControl;
import lombok.NonNull;
import lombok.Value;
@ -8,6 +9,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
public interface FileSystem extends Closeable, AutoCloseable {
@ -25,6 +27,8 @@ public interface FileSystem extends Closeable, AutoCloseable {
long size;
}
Optional<ShellProcessControl> getShell();
FileSystem open() throws Exception;
InputStream openInput(String file) throws Exception;

View file

@ -33,7 +33,7 @@ public class EditStoreAction implements ActionProvider {
@Override
public boolean isMajor() {
return true;
return false;
}
@Override