Rework prompts

This commit is contained in:
crschnick 2025-04-01 15:56:36 +00:00
parent 953d91ca42
commit a5027fa6a7
10 changed files with 220 additions and 19 deletions

View file

@ -11,6 +11,8 @@ import io.xpipe.app.password.PasswordManagerCommand;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.terminal.ExternalTerminalType;
import io.xpipe.app.terminal.TerminalMultiplexer;
import io.xpipe.app.terminal.TerminalPrompt;
import io.xpipe.app.terminal.TerminalPromptManager;
import io.xpipe.app.util.PlatformState;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.core.process.ShellScript;
@ -109,6 +111,12 @@ public class AppPrefs {
mapLocal(new SimpleObjectProperty<>(null), "terminalMultiplexer", TerminalMultiplexer.class, false);
final Property<Boolean> terminalPromptForRestart =
mapLocal(new SimpleBooleanProperty(true), "terminalPromptForRestart", Boolean.class, false);
final Property<TerminalPrompt> terminalPrompt =
mapLocal(new SimpleObjectProperty<>(null), "terminalPrompt", TerminalPrompt.class, false);
public ObservableValue<TerminalPrompt> terminalPrompt() {
return terminalPrompt;
}
public ObservableValue<UUID> terminalProxy() {
return terminalProxy;
@ -268,6 +276,7 @@ public class AppPrefs {
new VaultCategory(),
new SyncCategory(),
new TerminalCategory(),
new TerminalPromptCategory(),
new LoggingCategory(),
new EditorCategory(),
new RdpCategory(),

View file

@ -0,0 +1,63 @@
package io.xpipe.app.prefs;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.terminal.ExternalTerminalType;
import io.xpipe.app.terminal.TerminalMultiplexer;
import io.xpipe.app.terminal.TerminalPrompt;
import io.xpipe.app.util.*;
import javafx.beans.binding.Bindings;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import org.kordamp.ikonli.javafx.FontIcon;
public class TerminalPromptCategory extends AppPrefsCategory {
@Override
protected String getId() {
return "terminalPrompt";
}
@Override
protected Comp<?> create() {
return new OptionsBuilder()
.addTitle("terminalPromptConfiguration")
.sub(terminalPrompt())
.buildComp();
}
private OptionsBuilder terminalPrompt() {
var prefs = AppPrefs.get();
var choiceBuilder = OptionsChoiceBuilder.builder()
.property(prefs.terminalPrompt)
.allowNull(true)
.subclasses(TerminalPrompt.getClasses())
.transformer(entryComboBox -> {
var websiteLinkButton =
new ButtonComp(AppI18n.observable("website"), new FontIcon("mdi2w-web"), () -> {
var l = prefs.terminalPrompt().getValue().getDocsLink();
if (l != null) {
Hyperlinks.open(l);
}
});
websiteLinkButton.minWidth(Region.USE_PREF_SIZE);
websiteLinkButton.disable(Bindings.createBooleanBinding(
() -> {
return prefs.terminalPrompt.getValue() == null
|| prefs.terminalPrompt.getValue().getDocsLink() == null;
},
prefs.terminalPrompt));
var hbox = new HBox(entryComboBox, websiteLinkButton.createRegion());
HBox.setHgrow(entryComboBox, Priority.ALWAYS);
hbox.setSpacing(10);
return hbox;
})
.build();
var choice = choiceBuilder.build().buildComp();
choice.maxWidth(getCompWidth());
return new OptionsBuilder().nameAndDescription("terminalPrompt").addComp(choice, prefs.terminalPrompt);
}
}

View file

@ -9,6 +9,7 @@ import io.xpipe.app.util.OptionsBuilder;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellTerminalInitCommand;
import io.xpipe.core.store.FilePath;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
@ -19,8 +20,8 @@ import java.util.function.Function;
@SuperBuilder
public abstract class ConfigFileTerminalPrompt implements TerminalPrompt {
protected static OptionsBuilder createOptions(Property<ConfigFileTerminalPrompt> p, String extension, Function<String, ConfigFileTerminalPrompt> creator) {
var prop = new SimpleObjectProperty<String>();
protected static <T extends ConfigFileTerminalPrompt> OptionsBuilder createOptions(Property<T> p, String extension, Function<String, T> creator) {
var prop = new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().configuration : null);
return new OptionsBuilder()
.nameAndDescription("configuration")
.addComp(new IntegratedTextAreaComp(prop, false, "config", new SimpleStringProperty(extension)), prop)
@ -32,4 +33,21 @@ public abstract class ConfigFileTerminalPrompt implements TerminalPrompt {
}
protected String configuration;
protected abstract FilePath prepareCustomConfigFile(ShellControl sc) throws Exception;
protected abstract FilePath getDefaultConfigFile(ShellControl sc) throws Exception;
@Override
public ShellTerminalInitCommand terminalCommand(ShellControl sc) throws Exception {
FilePath configFile;
if (configuration == null || configuration.isBlank()) {
configFile = getDefaultConfigFile(sc);
} else {
configFile = prepareCustomConfigFile(sc);
}
return terminalCommand(sc, configFile);
}
protected abstract ShellTerminalInitCommand terminalCommand(ShellControl shellControl, FilePath config) throws Exception;
}

View file

@ -1,14 +1,29 @@
package io.xpipe.app.terminal;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.password.KeePassXcAssociationKey;
import io.xpipe.app.password.KeePassXcManager;
import io.xpipe.app.util.CommandSupport;
import io.xpipe.app.util.OptionsBuilder;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialect;
import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.process.ShellTerminalInitCommand;
import io.xpipe.core.store.FilePath;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
import java.util.Optional;
@Getter
@SuperBuilder
@ToString
@ -16,18 +31,67 @@ import lombok.extern.jackson.Jacksonized;
@JsonTypeName("starship")
public class StarshipTerminalPrompt extends ConfigFileTerminalPrompt {
public static OptionsBuilder createOptions(Property<StarshipTerminalPrompt> p) {
return createOptions(p, "toml", s -> StarshipTerminalPrompt.builder().configuration(s).build());
}
@Override
public String getDocsLink() {
return "";
}
@Override
public void checkSupported(ShellControl sc) throws Exception {
public void checkCanInstall(ShellControl sc) throws Exception {
CommandSupport.isInPathOrThrow(sc, "curl");
}
@Override
public ShellTerminalInitCommand setup(ShellControl shellControl) throws Exception {
return null;
public boolean checkIfInstalled(ShellControl sc) throws Exception {
if (sc.view().findProgram("starship").isPresent()) {
return true;
}
return false;
}
@Override
public void install(ShellControl sc) throws Exception {
var dir = getBinaryDirectory(sc).join("starship");
sc.command("curl -sS https://starship.rs/install.sh | sh /dev/stdin -y --bin-dir \"" + dir + "\" > /dev/null").execute();
}
@Override
public FilePath prepareCustomConfigFile(ShellControl sc) throws Exception {
var file = getConfigurationDirectory(sc).join("starship").join("starship.toml");
sc.view().writeTextFile(file, configuration);
return file;
}
@Override
public FilePath getDefaultConfigFile(ShellControl sc) throws Exception {
return sc.view().userHome().join(".config").join("starship.toml");
}
@Override
public ShellTerminalInitCommand terminalCommand(ShellControl shellControl, FilePath configFile) throws Exception {
return new ShellTerminalInitCommand() {
@Override
public Optional<String> terminalContent(ShellControl shellControl) throws Exception {
var s = shellControl.getShellDialect().getSetEnvironmentVariableCommand("STARSHIP_CONFIG", "") + "\n" + "eval \"$(starship init bash)\"";
return Optional.empty();
}
@Override
public boolean canPotentiallyRunInDialect(ShellDialect dialect) {
return false;
}
};
}
@Override
public List<ShellDialect> getSupportedDialects() {
return List.of(ShellDialects.BASH);
}
}

View file

@ -1,11 +1,11 @@
package io.xpipe.app.terminal;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.xpipe.app.util.ShellTemp;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellScript;
import io.xpipe.core.process.ShellDialect;
import io.xpipe.core.process.ShellTerminalInitCommand;
import io.xpipe.core.process.TerminalInitScriptConfig;
import io.xpipe.core.util.ValidationException;
import io.xpipe.core.store.FilePath;
import java.util.ArrayList;
import java.util.List;
@ -15,17 +15,34 @@ public interface TerminalPrompt {
static List<Class<?>> getClasses() {
var l = new ArrayList<Class<?>>();
l.add(TmuxTerminalMultiplexer.class);
l.add(ZellijTerminalMultiplexer.class);
l.add(ScreenTerminalMultiplexer.class);
l.add(StarshipTerminalPrompt.class);
return l;
}
default void checkComplete() throws ValidationException {}
String getDocsLink();
void checkSupported(ShellControl sc) throws Exception;
default FilePath getConfigurationDirectory(ShellControl sc) throws Exception {
return ShellTemp.createUserSpecificTempDataDirectory(sc, "prompt");
}
ShellTerminalInitCommand setup(ShellControl shellControl) throws Exception;
default FilePath getBinaryDirectory(ShellControl sc) throws Exception {
return ShellTemp.createUserSpecificTempDataDirectory(sc, "bin");
}
default void installIfNeeded(ShellControl sc) throws Exception {
if (checkIfInstalled(sc)) {
checkCanInstall(sc);
install(sc);
}
}
void checkCanInstall(ShellControl sc) throws Exception;
boolean checkIfInstalled(ShellControl sc) throws Exception;
void install(ShellControl sc) throws Exception;
ShellTerminalInitCommand terminalCommand(ShellControl shellControl) throws Exception;
List<ShellDialect> getSupportedDialects();
}

View file

@ -0,0 +1,27 @@
package io.xpipe.app.terminal;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.process.ShellControl;
public class TerminalPromptManager {
public static void configurePromptScript(ShellControl sc) {
var p = AppPrefs.get().terminalPrompt().getValue();
if (p == null) {
return;
}
var d = p.getSupportedDialects();
if (!d.contains(sc.getShellDialect())) {
return;
}
try {
p.installIfNeeded(sc);
sc.withInitSnippet(p.terminalCommand(sc));
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
}
}

View file

@ -5,6 +5,7 @@ import io.xpipe.app.password.PasswordManager;
import io.xpipe.app.storage.*;
import io.xpipe.app.terminal.ExternalTerminalType;
import io.xpipe.app.terminal.TerminalMultiplexer;
import io.xpipe.app.terminal.TerminalPrompt;
import io.xpipe.core.util.InPlaceSecretValue;
import io.xpipe.core.util.JacksonMapper;
@ -45,6 +46,7 @@ public class AppJacksonModule extends SimpleModule {
context.registerSubtypes(PasswordManager.getClasses());
context.registerSubtypes(TerminalMultiplexer.getClasses());
context.registerSubtypes(TerminalPrompt.getClasses());
context.addSerializers(_serializers);
context.addDeserializers(_deserializers);

View file

@ -6,9 +6,7 @@ import java.util.Optional;
public interface ShellTerminalInitCommand {
default Optional<String> terminalContent(ShellControl shellControl) throws Exception {
throw new UnsupportedOperationException();
}
Optional<String> terminalContent(ShellControl shellControl) throws Exception;
boolean canPotentiallyRunInDialect(ShellDialect dialect);
}

View file

@ -120,3 +120,4 @@ keePassXc=KeePassXC
zellij=zellij
tmux=tmux
onePassword=1Password
starship=Starship

View file

@ -1380,3 +1380,5 @@ retrievedPassword=Obtained: $PASSWORD$
refreshOpenpubkey=Refresh openpubkey identity
refreshOpenpubkeyDescription=Run opkssh refresh to make the openpubkey identity valid again
all=All
terminalPrompt=Terminal prompt
terminalPromptConfiguration=Terminal prompt configuration