This commit is contained in:
crschnick
2026-05-06 14:00:40 +00:00
parent 09c0a07b8c
commit b03210ee23
18 changed files with 118 additions and 27 deletions

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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<StoreCategoryWrapper> lastInformationCategory = new SimpleObjectProperty<>();
private final ObservableList<String> 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()

View File

@@ -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 -> {

View File

@@ -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()

View File

@@ -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());

View File

@@ -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();
}
};
}

View File

@@ -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<SecretCustomCommandStrategy> 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) {

View File

@@ -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);

View File

@@ -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),

View File

@@ -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();

View File

@@ -51,6 +51,8 @@ public abstract class ControllableWindowProcess {
public abstract Rect queryBounds();
public abstract Object getRawHandle();
public void updateBoundsState() {
if (!isActive()) {
return;

View File

@@ -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;

View File

@@ -86,6 +86,8 @@ public class RemoteDesktopDockView implements WindowDockListener {
return;
}
NativeWinWindowControl.MAIN_WINDOW.activate();
var controllable = e.getProcess();
controllable.show();
controllable.moveToFront();

View File

@@ -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);

View File

@@ -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

View File

@@ -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.