diff --git a/app/src/main/java/io/xpipe/app/ext/ProcessControlProvider.java b/app/src/main/java/io/xpipe/app/ext/ProcessControlProvider.java index dc524ca24..8d1f84a76 100644 --- a/app/src/main/java/io/xpipe/app/ext/ProcessControlProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/ProcessControlProvider.java @@ -87,4 +87,6 @@ public abstract class ProcessControlProvider { public abstract void addAskpassEnvironment( CommandBuilder b, String prefix, UUID requestId, UUID secretId, String... askpassName); + + public abstract void refreshWsl() throws Exception; } diff --git a/app/src/main/java/io/xpipe/app/ext/StartOnInitStore.java b/app/src/main/java/io/xpipe/app/ext/StartOnInitStore.java index 18258a15e..212565d05 100644 --- a/app/src/main/java/io/xpipe/app/ext/StartOnInitStore.java +++ b/app/src/main/java/io/xpipe/app/ext/StartOnInitStore.java @@ -29,6 +29,7 @@ public interface StartOnInitStore extends SelfReferentialStore, DataStore { ErrorEventFactory.fromThrowable(ex) .description("Unable to automatically start connection " + DataStorage.get().getStoreEntryDisplayName(i.getSelfEntry())) + .expected() .handle(); } } diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreCreationDialog.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreCreationDialog.java index 681a6f6aa..95bf3fafd 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreCreationDialog.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreCreationDialog.java @@ -2,9 +2,11 @@ package io.xpipe.app.hub.comp; import io.xpipe.app.comp.RegionBuilder; import io.xpipe.app.comp.base.*; +import io.xpipe.app.core.AppCache; import io.xpipe.app.core.AppFontSizes; import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppLayoutModel; +import io.xpipe.app.core.window.AppDialog; import io.xpipe.app.ext.DataStore; import io.xpipe.app.ext.DataStoreCreationCategory; import io.xpipe.app.ext.DataStoreProvider; @@ -168,6 +170,14 @@ public class StoreCreationDialog { return model; } + private static void showNotice() { + var shown = AppCache.getBoolean("creationMinimizeDialogShown", false); + if (!shown) { + AppDialog.information("creationMinimizeNotice"); + AppCache.update("creationMinimizeDialogShown", true); + } + } + private static ModalOverlay createModalOverlay(StoreCreationModel model) { var comp = new StoreCreationComp(model); comp.prefWidth(650); @@ -187,7 +197,12 @@ public class StoreCreationDialog { if (!model.isQuickConnect()) { var queueEntry = StoreCreationQueueEntry.of(model, modal); - modal.hideable(queueEntry); + + modal.setHideAction(() -> { + AppLayoutModel.get().getQueueEntries().add(queueEntry); + showNotice(); + }); + AppLayoutModel.get().getSelected().addListener((observable, oldValue, newValue) -> { if (model.getFinished().get() || !modal.isShowing()) { return; @@ -195,6 +210,7 @@ public class StoreCreationDialog { modal.hide(); AppLayoutModel.get().getQueueEntries().add(queueEntry); + showNotice(); }); modal.setRequireCloseButtonForClose(true); } diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryWrapper.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryWrapper.java index a0ff216e6..80ec7102c 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryWrapper.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreEntryWrapper.java @@ -73,6 +73,7 @@ public class StoreEntryWrapper { private final BooleanProperty pinToTop = new SimpleBooleanProperty(); private final IntegerProperty orderIndex = new SimpleIntegerProperty(); private final BooleanProperty effectiveBusy = new SimpleBooleanProperty(); + private final Property lastInformationCategory = new SimpleObjectProperty<>(); private final ObservableList tags = FXCollections.observableArrayList(); private boolean effectiveBusyProviderBound = false; @@ -223,11 +224,12 @@ public class StoreEntryWrapper { sessionActive.setValue(entry.getStore() instanceof SingletonSessionStore ss && entry.getStore() instanceof ShellStore && ss.isSessionRunning()); - category.setValue(StoreViewState.get().getCategories().getList().stream() + var newCat = StoreViewState.get().getCategories().getList().stream() .filter(storeCategoryWrapper -> storeCategoryWrapper.getCategory().getUuid().equals(entry.getCategoryUuid())) .findFirst() - .orElse(StoreViewState.get().getAllConnectionsCategory())); + .orElse(StoreViewState.get().getAllConnectionsCategory()); + category.setValue(newCat); perUser.setValue( !category.getValue().getRoot().equals(StoreViewState.get().getAllIdentitiesCategory()) && entry.isPerUserStore()); @@ -238,7 +240,14 @@ public class StoreEntryWrapper { var storeChanged = store.getValue() != entry.getStore(); store.setValue(entry.getStore()); - if (storeChanged || !information.isBound()) { + + var selectedCat = StoreViewState.get().getActiveCategory().getValue(); + var switchedCat = !selectedCat.equals(lastInformationCategory.getValue()); + lastInformationCategory.setValue(selectedCat); + + // Some infos depend on the section info, which might change the top level state based + // on the selected category + if (StoreViewState.get().isInitialized() && (storeChanged || !information.isBound() || switchedCat)) { information.unbind(); shownInformation.unbind(); if (entry.getValidity().isUsable() diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreIconChoiceComp.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreIconChoiceComp.java index eb00ae63f..7c61ddefd 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreIconChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreIconChoiceComp.java @@ -143,11 +143,12 @@ public class StoreIconChoiceComp extends ModalOverlayContentComp { struc.setPrefWidth(300); }); text.style(Styles.TEXT_SUBTLE); - text.visible(busy); + text.show(busy); var loading = new LoadingIconComp(busy, AppFontSizes::title); loading.prefWidth(50); loading.prefHeight(50); + loading.show(busy); var vbox = new VerticalComp(List.of(text, loading, refreshButton)).spacing(25); vbox.apply(struc -> { diff --git a/app/src/main/java/io/xpipe/app/hub/comp/StoreViewState.java b/app/src/main/java/io/xpipe/app/hub/comp/StoreViewState.java index ce140e7ae..2b8951ae3 100644 --- a/app/src/main/java/io/xpipe/app/hub/comp/StoreViewState.java +++ b/app/src/main/java/io/xpipe/app/hub/comp/StoreViewState.java @@ -340,6 +340,10 @@ public class StoreViewState { DataStorage.get().setSelectedCategory(newValue.getCategory()); batchModeSelection.getList().clear(); batchModeSelectionSet.clear(); + + Platform.runLater(() -> { + updateWrappers(); + }); }); var selected = AppCache.getNonNull("selectedCategory", UUID.class, () -> DataStorage.DEFAULT_CATEGORY_UUID); activeCategory.setValue(categories.getList().stream() diff --git a/app/src/main/java/io/xpipe/app/prefs/TerminalCategory.java b/app/src/main/java/io/xpipe/app/prefs/TerminalCategory.java index a45458b52..f062f33ca 100644 --- a/app/src/main/java/io/xpipe/app/prefs/TerminalCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/TerminalCategory.java @@ -7,14 +7,12 @@ import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppProperties; import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.ext.ProcessControlProvider; +import io.xpipe.app.ext.ScanProvider; import io.xpipe.app.ext.ShellStore; import io.xpipe.app.hub.comp.StoreChoiceComp; import io.xpipe.app.hub.comp.StoreViewState; import io.xpipe.app.issue.ErrorEventFactory; -import io.xpipe.app.platform.BindingsHelper; -import io.xpipe.app.platform.LabelGraphic; -import io.xpipe.app.platform.OptionsBuilder; -import io.xpipe.app.platform.OptionsChoiceBuilder; +import io.xpipe.app.platform.*; import io.xpipe.app.process.ShellScript; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntryRef; @@ -23,6 +21,8 @@ import io.xpipe.app.util.*; import io.xpipe.core.OsType; import javafx.beans.binding.Bindings; +import javafx.beans.property.ReadOnlyBooleanWrapper; +import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.geometry.Insets; @@ -244,14 +244,34 @@ public class TerminalCategory extends AppPrefsCategory { }); var proxyChoice = new DelayedInitComp( RegionBuilder.of(() -> { - var comp = new StoreChoiceComp<>( + var choice = new StoreChoiceComp<>( null, ref, ShellStore.class, r -> r.get().equals(DataStorage.get().local()) || TerminalProxyManager.canUseAsProxy(r), StoreViewState.get().getAllConnectionsCategory(), null); - return comp.build(); + choice.hgrow(); + + var refresh = new ButtonComp(null, new LabelGraphic.IconGraphic("mdi2r-refresh"), null); + refresh.describe(d -> d.nameKey("refresh")); + refresh.apply(button -> { + var disable = new SimpleBooleanProperty(); + button.disableProperty().bind(PlatformThread.sync(disable)); + button.setOnAction(event -> { + ThreadHelper.runFailableAsync(() -> { + BooleanScope.executeExclusive(disable, () -> { + ProcessControlProvider.get().refreshWsl(); + }); + }); + event.consume(); + }); + }); + refresh.hide(new ReadOnlyBooleanWrapper(OsType.ofLocal() != OsType.WINDOWS)); + + var box = new HorizontalComp(List.of(choice, refresh)); + box.spacing(12); + return box.build(); }), () -> StoreViewState.get() != null && StoreViewState.get().isInitialized()); proxyChoice.maxWidth(getCompWidth()); diff --git a/app/src/main/java/io/xpipe/app/rdp/ExternalRdpClient.java b/app/src/main/java/io/xpipe/app/rdp/ExternalRdpClient.java index 754984251..84045b8ef 100644 --- a/app/src/main/java/io/xpipe/app/rdp/ExternalRdpClient.java +++ b/app/src/main/java/io/xpipe/app/rdp/ExternalRdpClient.java @@ -80,7 +80,7 @@ public interface ExternalRdpClient extends PrefsValue { yield windowsApp; } case OsType.Windows ignored -> { - yield MstscRdpClient.builder().smartSizing(false).build(); + yield MstscRdpClient.builder().smartSizing(true).dock(true).build(); } }; } diff --git a/app/src/main/java/io/xpipe/app/secret/SecretCustomCommandStrategy.java b/app/src/main/java/io/xpipe/app/secret/SecretCustomCommandStrategy.java index 571e8523d..9e0b2b58a 100644 --- a/app/src/main/java/io/xpipe/app/secret/SecretCustomCommandStrategy.java +++ b/app/src/main/java/io/xpipe/app/secret/SecretCustomCommandStrategy.java @@ -1,16 +1,20 @@ package io.xpipe.app.secret; +import io.xpipe.app.comp.base.IntegratedTextAreaComp; import io.xpipe.app.comp.base.TextFieldComp; import io.xpipe.app.ext.ProcessControlProvider; import io.xpipe.app.ext.ValidationException; import io.xpipe.app.issue.ErrorEventFactory; import io.xpipe.app.platform.OptionsBuilder; +import io.xpipe.app.process.LocalShell; +import io.xpipe.app.process.ShellScript; import io.xpipe.app.util.Validators; import io.xpipe.core.InPlaceSecretValue; import javafx.beans.property.Property; import com.fasterxml.jackson.annotation.JsonTypeName; +import javafx.beans.property.ReadOnlyObjectWrapper; import lombok.Builder; import lombok.Value; import lombok.extern.jackson.Jacksonized; @@ -28,7 +32,9 @@ public class SecretCustomCommandStrategy implements SecretRetrievalStrategy { Property p, SecretStrategyChoiceConfig config) { var options = new OptionsBuilder(); var cmdProperty = options.map(p, SecretCustomCommandStrategy::getCommand); - return options.addComp(new TextFieldComp(cmdProperty), cmdProperty) + return options + .nameAndDescription("customCommandValue") + .addComp(IntegratedTextAreaComp.script(cmdProperty, new ReadOnlyObjectWrapper<>(LocalShell.getDialect().getScriptFileEnding()), true), cmdProperty) .nonNull() .bind( () -> { @@ -37,7 +43,7 @@ public class SecretCustomCommandStrategy implements SecretRetrievalStrategy { p); } - String command; + ShellScript command; @Override public void checkComplete() throws ValidationException { @@ -49,14 +55,14 @@ public class SecretCustomCommandStrategy implements SecretRetrievalStrategy { return new SecretQuery() { @Override public SecretQueryResult query(String prompt, boolean forceFocus) { - if (command == null || command.isBlank()) { + if (command == null || command.getValue().isBlank()) { throw ErrorEventFactory.expected(new IllegalStateException("No custom command specified")); } - try (var cc = ProcessControlProvider.get() + try (var sc = ProcessControlProvider.get() .createLocalProcessControl(true) - .command(command) .start()) { + var cc = sc.command(command); return new SecretQueryResult( InPlaceSecretValue.of(cc.readStdoutOrThrow()), SecretQueryState.NORMAL); } catch (Exception ex) { diff --git a/app/src/main/java/io/xpipe/app/terminal/TerminalDockView.java b/app/src/main/java/io/xpipe/app/terminal/TerminalDockView.java index 1914f03e2..64153e9ec 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalDockView.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalDockView.java @@ -29,7 +29,7 @@ public class TerminalDockView implements WindowDockListener { public synchronized void removeTerminal(TerminalView.TerminalSession s) { terminalInstances.removeIf(controllableTerminalSession -> - controllableTerminalSession.getTerminalProcess().equals(s.getTerminalProcess())); + controllableTerminalSession.equals(s)); } public synchronized boolean isActive() { @@ -125,11 +125,17 @@ public class TerminalDockView implements WindowDockListener { } public synchronized boolean closeOtherTerminals(UUID request) { + var owner = TerminalView.get().getSessions().stream() + .filter(shellSession -> shellSession.getRequest().equals(request)) + .map(shellSession -> shellSession.getTerminal()) + .findFirst().orElse(null); + if (owner == null) { + return false; + } + var others = terminalInstances.stream() .filter(terminal -> terminal.getTerminalProcess().isAlive()) - .filter(terminal -> TerminalView.get().getSessions().stream() - .noneMatch(shellSession -> shellSession.getRequest().equals(request) - && shellSession.getTerminal().equals(terminal))) + .filter(terminal -> !terminal.equals(owner)) .toList(); for (TerminalView.ControllableTerminalSession other : others) { closeTerminal(other); diff --git a/app/src/main/java/io/xpipe/app/terminal/TerminalPaneConfiguration.java b/app/src/main/java/io/xpipe/app/terminal/TerminalPaneConfiguration.java index ddd8cbf65..613e81c05 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalPaneConfiguration.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalPaneConfiguration.java @@ -86,11 +86,11 @@ public class TerminalPaneConfiguration { ShellDialects.POWERSHELL.terminalLauncherScript(request, title, alwaysPromptRestart)); var content = """ %s - echo 'Transcript started, output file is "sessions\\%s"' + echo 'Session logging is active, output file is "sessions\\%s"' Start-Transcript -Force -LiteralPath "%s" | Out-Null & "%s" Stop-Transcript | Out-Null - echo 'Transcript stopped, output file is "sessions\\%s"' + echo 'Session logging is finished, output file is "sessions\\%s"' """.formatted( TerminalLauncher.getTerminalRegisterCommand( request, LocalShell.getLocalPowershell().orElseThrow()), @@ -131,9 +131,9 @@ public class TerminalPaneConfiguration { : "script --quiet --command '%s' \"%s\"".formatted(command, logFile); var content = """ %s - echo "Transcript started, output file is sessions/%s" + echo "Session logging is active, output file is sessions/%s" %s - echo "Transcript stopped, output file is sessions/%s" + echo "Session logging is finished, output file is sessions/%s" cat "%s" | "%s" terminal-clean > "%s.txt" """.formatted( TerminalLauncher.getTerminalRegisterCommand(request, sc), diff --git a/app/src/main/java/io/xpipe/app/terminal/TerminalView.java b/app/src/main/java/io/xpipe/app/terminal/TerminalView.java index 4718b4914..4ac797ea9 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalView.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalView.java @@ -282,6 +282,19 @@ public class TerminalView { this.controllable = controllable; } + @Override + public boolean equals(Object o) { + if (!(o instanceof ControllableTerminalSession that)) { + return false; + } + return Objects.equals(terminalProcess.pid(), that.terminalProcess.pid()) && Objects.equals(controllable.getRawHandle(), that.controllable.getRawHandle()); + } + + @Override + public int hashCode() { + return Objects.hash(terminalProcess.pid(), controllable.getRawHandle()); + } + @Override public boolean isRunning() { return super.isRunning() && !controllable.isDestroyed(); diff --git a/app/src/main/java/io/xpipe/app/util/ControllableWindowProcess.java b/app/src/main/java/io/xpipe/app/util/ControllableWindowProcess.java index e8b48de96..0dedadc74 100644 --- a/app/src/main/java/io/xpipe/app/util/ControllableWindowProcess.java +++ b/app/src/main/java/io/xpipe/app/util/ControllableWindowProcess.java @@ -51,6 +51,8 @@ public abstract class ControllableWindowProcess { public abstract Rect queryBounds(); + public abstract Object getRawHandle(); + public void updateBoundsState() { if (!isActive()) { return; diff --git a/app/src/main/java/io/xpipe/app/util/ControllableWindowsProcess.java b/app/src/main/java/io/xpipe/app/util/ControllableWindowsProcess.java index f9a36ca83..2a459a40d 100644 --- a/app/src/main/java/io/xpipe/app/util/ControllableWindowsProcess.java +++ b/app/src/main/java/io/xpipe/app/util/ControllableWindowsProcess.java @@ -151,6 +151,11 @@ public final class ControllableWindowsProcess extends ControllableWindowProcess return control.getBounds(); } + @Override + public Object getRawHandle() { + return control.getWindowHandle(); + } + public void updateBoundsState() { if (!isActive()) { return; diff --git a/app/src/main/java/io/xpipe/app/util/RemoteDesktopDockView.java b/app/src/main/java/io/xpipe/app/util/RemoteDesktopDockView.java index 97cdf08d4..5c63268d7 100644 --- a/app/src/main/java/io/xpipe/app/util/RemoteDesktopDockView.java +++ b/app/src/main/java/io/xpipe/app/util/RemoteDesktopDockView.java @@ -86,6 +86,8 @@ public class RemoteDesktopDockView implements WindowDockListener { return; } + NativeWinWindowControl.MAIN_WINDOW.activate(); + var controllable = e.getProcess(); controllable.show(); controllable.moveToFront(); diff --git a/app/src/main/java/io/xpipe/app/util/ScanDialogBase.java b/app/src/main/java/io/xpipe/app/util/ScanDialogBase.java index d0a728bd6..0025eae4c 100644 --- a/app/src/main/java/io/xpipe/app/util/ScanDialogBase.java +++ b/app/src/main/java/io/xpipe/app/util/ScanDialogBase.java @@ -159,7 +159,7 @@ public class ScanDialogBase { so -> null, selected, scanOperation -> scanOperation.isDisabled(), - () -> available.size() > 3) + () -> available.stream().filter(sa -> !sa.isDisabled()).count() >= 2) .build(); stackPane.getChildren().add(r); diff --git a/lang/strings/fixed_en.properties b/lang/strings/fixed_en.properties index 3c58bce7b..a6a5efc57 100644 --- a/lang/strings/fixed_en.properties +++ b/lang/strings/fixed_en.properties @@ -54,7 +54,7 @@ nullPointer=Null Pointer discord=Discord slack=Slack github=GitHub -mstsc=Microsoft Terminal Services Client (MSTSC) +mstsc=Microsoft Terminal Services Client (mstsc) remmina=Remmina microsoftRemoteDesktopApp=Microsoft Remote Desktop.app bitwarden=Bitwarden diff --git a/lang/strings/translations_en.properties b/lang/strings/translations_en.properties index 9115f19b5..fc8385ccf 100644 --- a/lang/strings/translations_en.properties +++ b/lang/strings/translations_en.properties @@ -73,6 +73,8 @@ remove=Remove createNewCategory=New subcategory prompt=Prompt customCommand=Custom command +customCommandValue=Commands +customCommandValueDescription=The command/script to execute in the local shell other=Other setLock=Set lock selectConnection=Select connection @@ -2156,3 +2158,5 @@ connectionConfiguration=Connection configuration move=Move confirmDeletionTitle=Confirm deletion confirmDeletionContent=Do you want to delete the selected entries? +creationMinimizeNoticeTitle=Dialog minimized +creationMinimizeNoticeContent=The current dialog has been minimized. You can reopen it again by clicking on the minimized icon in the bottom right corner.