Rework default external application detection

This commit is contained in:
crschnick 2023-02-03 10:20:18 +00:00
parent 538249637a
commit 5afee8120d
11 changed files with 306 additions and 154 deletions

View file

@ -115,9 +115,9 @@ Furthermore, you also need the platform specific toolchains to be installed:
You can use the gradle wrapper to build and run the project:
- `gradlew app:run` will run the desktop application. You can set various useful properties in `app/build.gradle`
- `gradlew builtCli` will create a native image for the CLI application
- `gradlew buildCli` will create a native image for the CLI application
- `gradlew dist` will create a distributable production version in `dist/build/dist/base`.
To include this CLI executable in this build, make sure to run `gradlew builtCli` first
To include this CLI executable in this build, make sure to run `gradlew buildCli` first
- You can also run the CLI application in development mode with something like `gradlew :cli:clean :cli:run --args="daemon start"`.
Note here that you should always clean the CLI project first, as the native image plugin is a little buggy in that regard.
- `gradlew <project>:test` will run the tests of the specified project.

View file

@ -181,7 +181,7 @@ public class EditorState {
public void openInEditor(String file) {
var editor = AppPrefs.get().externalEditor().getValue();
if (editor == null || !editor.isSupported()) {
if (editor == null || !editor.isSelectable()) {
return;
}

View file

@ -61,9 +61,9 @@ public class AppPrefs {
private final ObjectProperty<SupportedLocale> languageInternal =
typed(new SimpleObjectProperty<>(SupportedLocale.ENGLISH), SupportedLocale.class);
public final Property<SupportedLocale> language = new SimpleObjectProperty<>(SupportedLocale.ENGLISH);
private final SingleSelectionField<SupportedLocale> languageControl =
Field.ofSingleSelectionType(languageList, languageInternal).render(() -> new TranslatableComboBoxControl<>());
private final SingleSelectionField<SupportedLocale> languageControl = Field.ofSingleSelectionType(
languageList, languageInternal)
.render(() -> new TranslatableComboBoxControl<>());
private final ObjectProperty<AppStyle.Theme> themeInternal =
typed(new SimpleObjectProperty<>(AppStyle.Theme.LIGHT), AppStyle.Theme.class);
@ -85,32 +85,36 @@ public class AppPrefs {
private final ObjectProperty<CloseBehaviour> closeBehaviour =
typed(new SimpleObjectProperty<>(CloseBehaviour.QUIT), CloseBehaviour.class);
private final SingleSelectionField<CloseBehaviour> closeBehaviourControl =
Field.ofSingleSelectionType(closeBehaviourList, closeBehaviour).render(() -> new TranslatableComboBoxControl<>());
private final ObjectProperty<ExternalEditorType> externalEditor =
typed(new SimpleObjectProperty<>(ExternalEditorType.getDefault()), ExternalEditorType.class);
private final SingleSelectionField<ExternalEditorType> externalEditorControl =
Field.ofSingleSelectionType(externalEditorList, externalEditor).render(() -> new TranslatableComboBoxControl<>());
private final SingleSelectionField<CloseBehaviour> closeBehaviourControl = Field.ofSingleSelectionType(
closeBehaviourList, closeBehaviour)
.render(() -> new TranslatableComboBoxControl<>());
// External editor
// ===============
private final StringProperty customEditorCommand = typed(new SimpleStringProperty(""), String.class);
final ObjectProperty<ExternalEditorType> externalEditor =
typed(new SimpleObjectProperty<>(), ExternalEditorType.class);
private final SingleSelectionField<ExternalEditorType> externalEditorControl = Field.ofSingleSelectionType(
externalEditorList, externalEditor)
.render(() -> new TranslatableComboBoxControl<>());
final StringProperty customEditorCommand = typed(new SimpleStringProperty(""), String.class);
private final StringField customEditorCommandControl = editable(
StringField.ofStringType(customEditorCommand).render(() -> new SimpleTextControl()),
externalEditor.isEqualTo(ExternalEditorType.CUSTOM));
private final IntegerProperty editorReloadTimeout = typed(new SimpleIntegerProperty(1000), Integer.class);
private final ObjectProperty<ExternalStartupBehaviour> externalStartupBehaviour = typed(
new SimpleObjectProperty<>(
ExternalStartupBehaviour.TRAY.isSupported()
ExternalStartupBehaviour.TRAY.isSelectable()
? ExternalStartupBehaviour.TRAY
: ExternalStartupBehaviour.BACKGROUND),
ExternalStartupBehaviour.class);
private final SingleSelectionField<ExternalStartupBehaviour> externalStartupBehaviourControl =
Field.ofSingleSelectionType(externalStartupBehaviourList, externalStartupBehaviour)
.render(() -> new TranslatableComboBoxControl<>());
// Automatically update
// ====================
private final BooleanProperty automaticallyUpdate =
typed(new SimpleBooleanProperty(AppDistributionType.get().supportsUpdate()), Boolean.class);
private final BooleanField automaticallyUpdateField = BooleanField.ofBooleanType(automaticallyUpdate)
@ -135,8 +139,8 @@ public class AppPrefs {
private final ObjectProperty<String> internalLogLevel =
typed(new SimpleObjectProperty<>(DEFAULT_LOG_LEVEL), String.class);
// Automatically update
// ====================
// Log level
// =========
private final ObjectProperty<String> effectiveLogLevel = LOG_LEVEL_FIXED
? new SimpleObjectProperty<>(System.getProperty(LOG_LEVEL_PROP).toLowerCase())
: internalLogLevel;
@ -144,6 +148,8 @@ public class AppPrefs {
logLevelList, effectiveLogLevel)
.editable(!LOG_LEVEL_FIXED)
.render(() -> new SimpleComboBoxControl<>());
// Developer mode
// ==============
private final BooleanProperty internalDeveloperMode = typed(new SimpleBooleanProperty(false), Boolean.class);
private final BooleanProperty effectiveDeveloperMode = System.getProperty(DEVELOPER_MODE_PROP) != null
? new SimpleBooleanProperty(Boolean.parseBoolean(System.getProperty(DEVELOPER_MODE_PROP)))
@ -176,6 +182,70 @@ public class AppPrefs {
developerDisableConnectorInstallationVersionCheck)
.render(() -> new ToggleControl());
public ReadOnlyProperty<CloseBehaviour> closeBehaviour() {
return closeBehaviour;
}
public ReadOnlyProperty<ExternalEditorType> externalEditor() {
return externalEditor;
}
public ObservableValue<String> customEditorCommand() {
return customEditorCommand;
}
public final ReadOnlyIntegerProperty editorReloadTimeout() {
return editorReloadTimeout;
}
public ReadOnlyProperty<ExternalStartupBehaviour> externalStartupBehaviour() {
return externalStartupBehaviour;
}
public ReadOnlyBooleanProperty automaticallyUpdate() {
return automaticallyUpdate;
}
public ReadOnlyBooleanProperty updateToPrereleases() {
return updateToPrereleases;
}
public ReadOnlyBooleanProperty confirmDeletions() {
return confirmDeletions;
}
public ObservableValue<Path> storageDirectory() {
return effectiveStorageDirectory;
}
public ReadOnlyProperty<String> logLevel() {
return effectiveLogLevel;
}
public ObservableValue<Boolean> developerMode() {
return effectiveDeveloperMode;
}
public ReadOnlyBooleanProperty developerDisableUpdateVersionCheck() {
return developerDisableUpdateVersionCheck;
}
public ReadOnlyBooleanProperty developerDisableGuiRestrictions() {
return developerDisableGuiRestrictions;
}
public ReadOnlyBooleanProperty developerDisableConnectorInstallationVersionCheck() {
return developerDisableConnectorInstallationVersionCheck;
}
public ReadOnlyBooleanProperty developerShowHiddenProviders() {
return developerShowHiddenProviders;
}
public ReadOnlyBooleanProperty developerShowHiddenEntries() {
return developerShowHiddenEntries;
}
private AppPreferencesFx preferencesFx;
private boolean controlsSetup;
@ -194,6 +264,8 @@ public class AppPrefs {
public static void init() {
INSTANCE = new AppPrefs();
INSTANCE.preferencesFx.loadSettings();
INSTANCE.initValues();
PrefsProvider.getAll().forEach(prov -> prov.init());
}
public static AppPrefs get() {
@ -246,8 +318,11 @@ public class AppPrefs {
save();
}
// Log level
// =========
public void initValues() {
if (externalEditor.get() == null) {
ExternalEditorType.detectDefault();
}
}
public void save() {
preferencesFx.saveSettings();
@ -257,76 +332,6 @@ public class AppPrefs {
preferencesFx.discardChanges();
}
public ReadOnlyProperty<CloseBehaviour> closeBehaviour() {
return closeBehaviour;
}
public ReadOnlyProperty<ExternalEditorType> externalEditor() {
return externalEditor;
}
public ObservableValue<String> customEditorCommand() {
return customEditorCommand;
}
public final ReadOnlyIntegerProperty editorReloadTimeout() {
return editorReloadTimeout;
}
public ReadOnlyProperty<ExternalStartupBehaviour> externalStartupBehaviour() {
return externalStartupBehaviour;
}
public ReadOnlyBooleanProperty automaticallyUpdate() {
return automaticallyUpdate;
}
// Developer mode
// ==============
public ReadOnlyBooleanProperty updateToPrereleases() {
return updateToPrereleases;
}
public ReadOnlyBooleanProperty confirmDeletions() {
return confirmDeletions;
}
public ObservableValue<Path> storageDirectory() {
return effectiveStorageDirectory;
}
// Developer options
// ====================
public ReadOnlyProperty<String> logLevel() {
return effectiveLogLevel;
}
public ObservableValue<Boolean> developerMode() {
return effectiveDeveloperMode;
}
public ReadOnlyBooleanProperty developerDisableUpdateVersionCheck() {
return developerDisableUpdateVersionCheck;
}
public ReadOnlyBooleanProperty developerDisableGuiRestrictions() {
return developerDisableGuiRestrictions;
}
public ReadOnlyBooleanProperty developerDisableConnectorInstallationVersionCheck() {
return developerDisableConnectorInstallationVersionCheck;
}
public ReadOnlyBooleanProperty developerShowHiddenProviders() {
return developerShowHiddenProviders;
}
public ReadOnlyBooleanProperty developerShowHiddenEntries() {
return developerShowHiddenEntries;
}
public Class<?> getSettingType(String breadcrumb) {
var found = classMap.get(getSetting(breadcrumb).valueProperty());
if (found == null) {
@ -391,15 +396,15 @@ public class AppPrefs {
Setting.of("useSystemFont", useSystemFontInternal),
Setting.of("tooltipDelay", tooltipDelayInternal, tooltipDelayMin, tooltipDelayMax),
Setting.of("fontSize", fontSizeInternal, fontSizeMin, fontSizeMax)),
Group.of(
"windowOptions",
Setting.of("saveWindowLocation", saveWindowLocationInternal))),
Group.of("windowOptions", Setting.of("saveWindowLocation", saveWindowLocationInternal))),
Category.of(
"integrations",
Group.of(
"editor",
Setting.of("defaultProgram", externalEditorControl, externalEditor),
Setting.of("customEditorCommand", customEditorCommandControl, customEditorCommand).applyVisibility( VisibilityProperty.of(externalEditor.isEqualTo(ExternalEditorType.CUSTOM))),
Setting.of("customEditorCommand", customEditorCommandControl, customEditorCommand)
.applyVisibility(VisibilityProperty.of(
externalEditor.isEqualTo(ExternalEditorType.CUSTOM))),
Setting.of(
"editorReloadTimeout",
editorReloadTimeout,

View file

@ -32,7 +32,7 @@ public enum CloseBehaviour implements PrefsChoiceValue {
this.exit = exit;
}
public boolean isSupported() {
public boolean isSelectable() {
return true;
}
}

View file

@ -2,7 +2,6 @@ package io.xpipe.app.prefs;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellTypes;
import io.xpipe.core.store.ShellStore;
import io.xpipe.extension.prefs.PrefsChoiceValue;
import io.xpipe.extension.util.ApplicationHelper;
import io.xpipe.extension.util.WindowsRegistry;
@ -11,7 +10,9 @@ import lombok.Getter;
import org.apache.commons.lang3.SystemUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@ -20,13 +21,24 @@ import java.util.Optional;
public abstract class ExternalEditorType implements PrefsChoiceValue {
public static final ExternalEditorType NOTEPAD = new WindowsFullPathType("app.notepad") {
@Override
protected Optional<Path> determinePath() {
return Optional.of(Path.of(System.getenv("SystemRoot") + "\\System32\\notepad.exe"));
}
};
public static final ExternalEditorType NOTEPADPLUSPLUS_WINDOWS = new WindowsFullPathType("app.notepad++Windows") {
public static final ExternalEditorType VSCODE = new WindowsFullPathType("app.vscode") {
@Override
protected Optional<Path> determinePath() {
return Optional.of(Path.of(System.getenv("LOCALAPPDATA"))
.resolve("Programs")
.resolve("Microsoft VS Code")
.resolve("bin")
.resolve("code.cmd"));
}
};
public static final ExternalEditorType NOTEPADPLUSPLUS_WINDOWS = new WindowsFullPathType("app.notepad++") {
@Override
protected Optional<Path> determinePath() {
@ -40,66 +52,80 @@ public abstract class ExternalEditorType implements PrefsChoiceValue {
return launcherDir.map(Path::of);
}
};
public static final ExternalEditorType NOTEPADPLUSPLUS_LINUX =
new LinuxPathType("app.notepad++Linux", "notepad++") {};
public static final ExternalEditorType KATE = new LinuxPathType("app.kate", "kate") {};
public static final PathType NOTEPADPLUSPLUS_LINUX = new PathType("app.notepad++", "notepad++");
public static final PathType VSCODE_LINUX = new PathType("app.vscode", "code");
public static final PathType KATE = new PathType("app.kate", "kate");
public static final PathType GEDIT = new PathType("app.gedit", "gedit");
public static final PathType LEAFPAD = new PathType("app.leafpad", "leafpad");
public static final PathType MOUSEPAD = new PathType("app.mousepad", "mousepad");
public static final PathType PLUMA = new PathType("app.pluma", "pluma");
public static final ExternalEditorType TEXT_EDIT = new MacOsFullPathType("app.textEdit") {
@Override
protected Path determinePath() {
return Path.of("/Applications/TextEdit.app");
}
};
public static final ExternalEditorType NOTEPADPP_MACOS = new MacOsFullPathType("app.notepad++") {
@Override
protected Path determinePath() {
return Path.of("/Applications/TextEdit.app");
}
};
public static final ExternalEditorType SUBLIME_MACOS = new MacOsFullPathType("app.sublime") {
@Override
protected Path determinePath() {
return Path.of("/Applications/Sublime.app");
}
};
public static final ExternalEditorType VSCODE_MACOS = new MacOsFullPathType("app.vscode") {
@Override
protected Path determinePath() {
return Path.of("/Applications/VSCode.app");
}
};
public static final ExternalEditorType CUSTOM = new ExternalEditorType("app.custom") {
@Override
public void launch(Path file) throws IOException {
var fileName = SystemUtils.IS_OS_WINDOWS ? " \"" + file + "\"" : file;
var cmd = AppPrefs.get().customEditorCommand().getValue();
var fullCmd = cmd + " " + fileName;
Runtime.getRuntime()
.exec(ShellTypes.getPlatformDefault()
.executeCommandListWithShell(fullCmd)
.toArray(String[]::new));
public void launch(Path file) throws Exception {
var customCommand = AppPrefs.get().customEditorCommand().getValue();
if (customCommand == null || customCommand.trim().isEmpty()) {
return;
}
var format = customCommand.contains("$file") ? customCommand : customCommand + " $file";
var fileString = file.toString().contains(" ") ? "\"" + file + "\"" : file.toString();
ApplicationHelper.executeLocalApplication(format.replace("$file",fileString));
}
@Override
public boolean isSupported() {
public boolean isSelectable() {
return true;
}
};
public static final ExternalEditorType TEXT_EDIT = new ExternalEditorType("app.textEdit") {
@Override
public void launch(Path file) throws Exception {
var fullCmd = "/Applications/TextEdit.app/Contents/MacOS/TextEdit \"" + file.toString() + "\"";
ShellStore.withLocal(pc -> {
pc.executeSimpleCommand(fullCmd);
});
}
@Override
public boolean isSupported() {
return OsType.getLocal().equals(OsType.MAC);
}
};
public static final List<ExternalEditorType> ALL =
List.of(NOTEPAD, NOTEPADPLUSPLUS_WINDOWS, NOTEPADPLUSPLUS_LINUX, KATE, TEXT_EDIT, CUSTOM);
private String id;
public static ExternalEditorType getDefault() {
if (OsType.getLocal().equals(OsType.MAC)) {
return TEXT_EDIT;
}
return OsType.getLocal().equals(OsType.WINDOWS) ? NOTEPAD : KATE;
}
public abstract void launch(Path file) throws Exception;
public abstract boolean isSupported();
public abstract boolean isSelectable();
public abstract static class LinuxPathType extends ExternalEditorType {
public static class PathType extends ExternalEditorType {
private final String command;
public LinuxPathType(String id, String command) {
public PathType(String id, String command) {
super(id);
this.command = command;
}
@ -111,7 +137,7 @@ public abstract class ExternalEditorType implements PrefsChoiceValue {
}
@Override
public boolean isSupported() {
public boolean isSelectable() {
return OsType.getLocal().equals(OsType.LINUX);
}
}
@ -131,17 +157,103 @@ public abstract class ExternalEditorType implements PrefsChoiceValue {
throw new IOException("Unable to find installation of " + getId());
}
ApplicationHelper.executeLocalApplication(getCommand(path.get(), file));
}
protected String getCommand(Path p, Path file) {
var cmd = "\"" + p + "\"";
return "start \"\" " + cmd + " \"" + file + "\"";
ApplicationHelper.executeLocalApplication(List.of(path.get().toString(), file.toString()));
}
@Override
public boolean isSupported() {
public boolean isSelectable() {
return OsType.getLocal().equals(OsType.WINDOWS);
}
@Override
public boolean isAvailable() {
var path = determinePath();
return path.isPresent() && Files.exists(path.get());
}
}
public abstract static class MacOsFullPathType extends ExternalEditorType {
public MacOsFullPathType(String id) {
super(id);
}
protected abstract Path determinePath();
@Override
public void launch(Path file) throws Exception {
var path = determinePath();
ApplicationHelper.executeLocalApplication(List.of("open", path.toString(), file.toString()));
}
@Override
public boolean isSelectable() {
return OsType.getLocal().equals(OsType.MAC);
}
@Override
public boolean isAvailable() {
var path = determinePath();
return Files.exists(path);
}
}
public static final List<ExternalEditorType> WINDOWS_EDITORS = List.of(VSCODE, NOTEPADPLUSPLUS_WINDOWS, NOTEPAD);
public static final List<PathType> LINUX_EDITORS =
List.of(VSCODE_LINUX, NOTEPADPLUSPLUS_LINUX, KATE, GEDIT, PLUMA, LEAFPAD, MOUSEPAD);
public static final List<ExternalEditorType> MACOS_EDITORS =
List.of(VSCODE_MACOS, SUBLIME_MACOS, NOTEPADPP_MACOS, TEXT_EDIT);
public static final List<ExternalEditorType> ALL = new ArrayList<>();
static {
if (OsType.getLocal().equals(OsType.WINDOWS)) {
ALL.addAll(WINDOWS_EDITORS);
}
if (OsType.getLocal().equals(OsType.LINUX)) {
ALL.addAll(LINUX_EDITORS);
}
if (OsType.getLocal().equals(OsType.MAC)) {
ALL.addAll(MACOS_EDITORS);
}
ALL.add(CUSTOM);
}
public static void detectDefault() {
var typeProperty = AppPrefs.get().externalEditor;
var customProperty = AppPrefs.get().customEditorCommand;
if (OsType.getLocal().equals(OsType.WINDOWS)) {
typeProperty.set(WINDOWS_EDITORS.stream()
.filter(externalEditorType -> externalEditorType.isAvailable())
.findFirst()
.orElse(null));
}
if (OsType.getLocal().equals(OsType.LINUX)) {
var env = System.getenv("VISUAL");
if (env != null) {
var found = LINUX_EDITORS.stream()
.filter(externalEditorType -> externalEditorType.command.equalsIgnoreCase(env))
.findFirst()
.orElse(null);
if (found == null) {
typeProperty.set(CUSTOM);
customProperty.set(env);
} else {
typeProperty.set(found);
}
} else {
typeProperty.set(LINUX_EDITORS.stream()
.filter(externalEditorType -> externalEditorType.isAvailable())
.findFirst()
.orElse(null));
}
}
if (OsType.getLocal().equals(OsType.MAC)) {
typeProperty.set(MACOS_EDITORS.stream()
.filter(externalEditorType -> externalEditorType.isAvailable())
.findFirst()
.orElse(null));
}
}
}

View file

@ -15,7 +15,7 @@ public enum ExternalStartupBehaviour implements PrefsChoiceValue {
private final String id;
private final OperationMode mode;
public boolean isSupported() {
public boolean isSelectable() {
return true;
}
}

View file

@ -78,6 +78,8 @@ public interface ShellType {
List<String> executeCommandListWithShell(String cmd);
List<String> executeCommandListWithShell(List<String> cmd);
List<String> getMkdirsCommand(String dirs);
String getFileReadCommand(String file);

View file

@ -141,6 +141,14 @@ public class ShellTypes {
return List.of("cmd", "/C", cmd.replaceAll("[\\^]", "^$0"));
}
@Override
public List<String> executeCommandListWithShell(List<String> cmd) {
var list = new ArrayList<String>();
list.addAll(List.of("cmd", "/C"));
list.addAll(cmd.stream().map(s -> s.replaceAll(" ", "^ ")).toList());
return list;
}
@Override
public List<String> getMkdirsCommand(String dirs) {
return List.of("(", "if", "not", "exist", dirs, "mkdir", dirs, ")");
@ -230,6 +238,11 @@ public class ShellTypes {
@Value
public static class PowerShell implements ShellType {
@Override
public List<String> executeCommandListWithShell(List<String> cmd) {
return List.of("powershell", "-Command", flatten(cmd));
}
@Override
public void disableHistory(ShellProcessControl pc) throws Exception {
pc.executeLine("Set-PSReadLineOption -HistorySaveStyle SaveNothing");
@ -416,6 +429,11 @@ public class ShellTypes {
public abstract static class PosixBase implements ShellType {
@Override
public List<String> executeCommandListWithShell(List<String> cmd) {
return List.of(getExecutable(), "-c", flatten(cmd));
}
@Override
public String getInitFileOpenCommand(String file) {
return getName() + " --rcfile \"" + file + "\"";

View file

@ -39,11 +39,15 @@ public interface PrefsChoiceValue extends Translatable {
throw new AssertionError();
}
return all.stream().filter(t -> ((PrefsChoiceValue) t).isSupported()).toList();
return all.stream().filter(t -> ((PrefsChoiceValue) t).isSelectable()).toList();
}
}
default boolean isSupported() {
default boolean isAvailable() {
return true;
}
default boolean isSelectable() {
return true;
}

View file

@ -34,4 +34,6 @@ public abstract class PrefsProvider {
}
public abstract void addPrefs(PrefsHandler handler);
public abstract void init();
}

View file

@ -2,18 +2,27 @@ package io.xpipe.extension.util;
import io.xpipe.core.process.ShellProcessControl;
import io.xpipe.core.process.ShellTypes;
import io.xpipe.core.store.ShellStore;
import io.xpipe.extension.event.TrackEvent;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
public class ApplicationHelper {
public static void executeLocalApplication(String s) throws Exception {
var args = ShellTypes.getPlatformDefault().executeCommandListWithShell(s);
var p = new ProcessBuilder(args).redirectOutput(ProcessBuilder.Redirect.DISCARD).start();
var error = new String(p.getErrorStream().readAllBytes(), StandardCharsets.UTF_8);
if (p.waitFor() != 0) {
throw new IOException(error);
TrackEvent.withDebug("proc", "Executing local application").elements(args).handle();
try (var c = ShellStore.local().create().command(s).start()) {
c.discardOrThrow();
}
}
public static void executeLocalApplication(List<String> s) throws Exception {
var args = ShellTypes.getPlatformDefault().executeCommandListWithShell(s);
TrackEvent.withDebug("proc", "Executing local application").elements(args).handle();
try (var c = ShellStore.local().create().command(s).start()) {
c.discardOrThrow();
}
}