diff --git a/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java b/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java index 78c254356..68fa5e2da 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java @@ -53,7 +53,7 @@ public class MarkdownComp extends Comp> { private Path getHtmlFile(String markdown) { if (TEMP == null) { - TEMP = ShellTemp.getLocalTempDataDirectory("wv"); + TEMP = ShellTemp.getLocalTempDataDirectory("webview"); } if (markdown == null) { diff --git a/app/src/main/java/io/xpipe/app/terminal/ClinkHelper.java b/app/src/main/java/io/xpipe/app/terminal/ClinkHelper.java new file mode 100644 index 000000000..61559664c --- /dev/null +++ b/app/src/main/java/io/xpipe/app/terminal/ClinkHelper.java @@ -0,0 +1,57 @@ +package io.xpipe.app.terminal; + +import io.xpipe.app.util.GithubReleaseDownloader; +import io.xpipe.app.util.HttpHelper; +import io.xpipe.app.util.ShellTemp; +import io.xpipe.core.process.ShellControl; +import io.xpipe.core.store.FilePath; +import io.xpipe.core.util.JacksonMapper; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; + +public class ClinkHelper { + + private static String downloadUrl = null; + private static Path downloadFile = null; + + public static FilePath getTargetDir(ShellControl sc) throws Exception { + var targetDir = ShellTemp.createUserSpecificTempDataDirectory(sc, null).join("bin", "clink"); + return targetDir; + } + + public static boolean checkIfInstalled(ShellControl sc) throws Exception { + if (sc.view().findProgram("clink").isPresent()) { + return true; + } + + var targetDir = getTargetDir(sc); + return sc.view().fileExists(targetDir.join("clink_x64.exe")); + } + + public static void install(ShellControl sc) throws Exception { + var targetDir = getTargetDir(sc); + sc.view().mkdir(targetDir); + var temp = GithubReleaseDownloader.getDownloadTempFile("chrisant996/clink", "clink.zip", name -> name.endsWith(".zip") && !name.endsWith("symbols.zip")); + try (var fs = FileSystems.newFileSystem(temp)) { + var exeFile = fs.getPath("clink_x64.exe"); + var exeBytes = Files.readAllBytes(exeFile); + sc.view().writeStreamFile(targetDir.join("clink_x64.exe"), new ByteArrayInputStream(exeBytes), exeBytes.length); + + var batFile = fs.getPath("clink.bat"); + var batBytes = Files.readAllBytes(batFile); + sc.view().writeStreamFile(targetDir.join("clink.bat"), new ByteArrayInputStream(batBytes), batBytes.length); + + var dllFile = fs.getPath("clink_dll_x64.dll"); + var dllBytes = Files.readAllBytes(dllFile); + sc.view().writeStreamFile(targetDir.join("clink_dll_x64.dll"), new ByteArrayInputStream(dllBytes), dllBytes.length); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/terminal/ConfigFileTerminalPrompt.java b/app/src/main/java/io/xpipe/app/terminal/ConfigFileTerminalPrompt.java index ee976192e..1766f77f2 100644 --- a/app/src/main/java/io/xpipe/app/terminal/ConfigFileTerminalPrompt.java +++ b/app/src/main/java/io/xpipe/app/terminal/ConfigFileTerminalPrompt.java @@ -1,13 +1,10 @@ package io.xpipe.app.terminal; -import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.comp.base.IntegratedTextAreaComp; -import io.xpipe.app.core.AppI18n; -import io.xpipe.app.password.KeePassXcAssociationKey; -import io.xpipe.app.password.KeePassXcManager; 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.ShellScript; import io.xpipe.core.process.ShellTerminalInitCommand; import io.xpipe.core.store.FilePath; import javafx.beans.property.Property; @@ -15,6 +12,8 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import lombok.experimental.SuperBuilder; +import java.util.List; +import java.util.Optional; import java.util.function.Function; @SuperBuilder @@ -23,8 +22,9 @@ public abstract class ConfigFileTerminalPrompt implements TerminalPrompt { protected static OptionsBuilder createOptions(Property p, String extension, Function 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) + .nameAndDescription("terminalPromptConfig") + .addComp(new IntegratedTextAreaComp(prop, false, p.getValue() != null ? p.getValue().getId() : "config", + new SimpleStringProperty(extension)).prefHeight(400), prop) .bind( () -> { return creator.apply(prop.getValue()); @@ -39,15 +39,32 @@ public abstract class ConfigFileTerminalPrompt implements TerminalPrompt { 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); + public ShellTerminalInitCommand terminalCommand() throws Exception { + return new ShellTerminalInitCommand() { + @Override + public Optional terminalContent(ShellControl shellControl) throws Exception { + if (!installIfNeeded(shellControl)) { + return Optional.empty(); + } + + FilePath configFile; + if (configuration == null || configuration.isBlank()) { + configFile = getDefaultConfigFile(shellControl); + } else { + configFile = prepareCustomConfigFile(shellControl); + shellControl.view().writeTextFile(configFile, configuration); + } + + var s = shellControl.getShellDialect().addToPathVariableCommand(List.of(getBinaryDirectory(shellControl).toString()), false); + return Optional.of(s + "\n" + setupTerminalCommand(shellControl, configFile).toString()); + } + + @Override + public boolean canPotentiallyRunInDialect(ShellDialect dialect) { + return getSupportedDialects().contains(dialect); + } + }; } - protected abstract ShellTerminalInitCommand terminalCommand(ShellControl shellControl, FilePath config) throws Exception; + protected abstract ShellScript setupTerminalCommand(ShellControl shellControl, FilePath config) throws Exception; } diff --git a/app/src/main/java/io/xpipe/app/terminal/OhMyPoshTerminalPrompt.java b/app/src/main/java/io/xpipe/app/terminal/OhMyPoshTerminalPrompt.java new file mode 100644 index 000000000..61f2469ee --- /dev/null +++ b/app/src/main/java/io/xpipe/app/terminal/OhMyPoshTerminalPrompt.java @@ -0,0 +1,117 @@ +package io.xpipe.app.terminal; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.app.util.CommandSupport; +import io.xpipe.app.util.GithubReleaseDownloader; +import io.xpipe.app.util.OptionsBuilder; +import io.xpipe.core.process.*; +import io.xpipe.core.store.FilePath; +import javafx.beans.property.Property; +import lombok.Getter; +import lombok.ToString; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@SuperBuilder +@ToString +@Jacksonized +@JsonTypeName("starship") +public class OhMyPoshTerminalPrompt extends ConfigFileTerminalPrompt { + + public static OptionsBuilder createOptions(Property p) { + return createOptions(p, "toml", s -> OhMyPoshTerminalPrompt.builder().configuration(s).build()); + } + + @Override + public String getDocsLink() { + return "https://starship.rs/guide/"; + } + + @Override + public String getId() { + return "starship"; + } + + @Override + public void checkCanInstall(ShellControl sc) throws Exception { + if (sc.getOsType() != OsType.WINDOWS) { + CommandSupport.isInPathOrThrow(sc, "curl"); + } + } + + @Override + public boolean checkIfInstalled(ShellControl sc) throws Exception { + if (sc.getShellDialect() == ShellDialects.CMD && !ClinkHelper.checkIfInstalled(sc)) { + return false; + } + + if (sc.view().findProgram("starship").isPresent()) { + return true; + } + + var extension = OsType.getLocal() == OsType.WINDOWS ? ".exe" : ""; + return sc.view().fileExists(getBinaryDirectory(sc).join("starship" + extension)); + } + + @Override + public void install(ShellControl sc) throws Exception { + if (sc.getShellDialect() == ShellDialects.CMD) { + ClinkHelper.install(sc); + var configDir = getConfigurationDirectory(sc); + sc.view().mkdir(configDir); + sc.view().writeTextFile(configDir.join("starship.lua"), "load(io.popen('starship init cmd'):read(\"*a\"))()"); + } + + var dir = getBinaryDirectory(sc); + sc.view().mkdir(dir); + if (sc.getOsType() == OsType.WINDOWS) { + var file = GithubReleaseDownloader.getDownloadTempFile( + "JanDeDobbeleer/oh-my-posh", + "posh-windows-amd64.exe", + s -> s.equals("posh-windows-amd64.exe")); + sc.view().transferLocalFile(file, dir.join("starship.exe")); + } else { + 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.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 + protected ShellScript setupTerminalCommand(ShellControl shellControl, FilePath config) throws Exception { + var lines = new ArrayList(); + if (shellControl.getShellDialect() == ShellDialects.CMD) { + lines.add(shellControl.getShellDialect().addToPathVariableCommand(List.of(ClinkHelper.getTargetDir(shellControl).toString()), false)); + } + lines.add(shellControl.getShellDialect().getSetEnvironmentVariableCommand("STARSHIP_CONFIG", config.toString())); + if (shellControl.getShellDialect() == ShellDialects.CMD) { + lines.add("clink inject --quiet --profile \"" + getConfigurationDirectory(shellControl) + "\""); + } else if (ShellDialects.isPowershell(shellControl)) { + lines.add("Invoke-Expression (&starship init powershell)"); + } else if (shellControl.getShellDialect() == ShellDialects.FISH) { + lines.add("starship init fish | source"); + } else { + lines.add("eval \"$(starship init " + shellControl.getShellDialect().getId() + ")\""); + } + return ShellScript.lines(lines); + } + + @Override + public List getSupportedDialects() { + return List.of(ShellDialects.BASH, ShellDialects.ZSH, ShellDialects.FISH, ShellDialects.CMD, ShellDialects.POWERSHELL, ShellDialects.POWERSHELL_CORE); + } +} diff --git a/app/src/main/java/io/xpipe/app/terminal/StarshipTerminalPrompt.java b/app/src/main/java/io/xpipe/app/terminal/StarshipTerminalPrompt.java index d7afc9690..498a33cf0 100644 --- a/app/src/main/java/io/xpipe/app/terminal/StarshipTerminalPrompt.java +++ b/app/src/main/java/io/xpipe/app/terminal/StarshipTerminalPrompt.java @@ -1,28 +1,21 @@ 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.GithubReleaseDownloader; 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.process.*; 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.nio.file.FileSystems; +import java.nio.file.Files; +import java.util.ArrayList; import java.util.List; -import java.util.Optional; @Getter @SuperBuilder @@ -37,32 +30,62 @@ public class StarshipTerminalPrompt extends ConfigFileTerminalPrompt { @Override public String getDocsLink() { - return ""; + return "https://starship.rs/guide/"; + } + + @Override + public String getId() { + return "starship"; } @Override public void checkCanInstall(ShellControl sc) throws Exception { - CommandSupport.isInPathOrThrow(sc, "curl"); + if (sc.getOsType() != OsType.WINDOWS) { + CommandSupport.isInPathOrThrow(sc, "curl"); + } } @Override public boolean checkIfInstalled(ShellControl sc) throws Exception { + if (sc.getShellDialect() == ShellDialects.CMD && !ClinkHelper.checkIfInstalled(sc)) { + return false; + } + if (sc.view().findProgram("starship").isPresent()) { return true; } - return false; + var extension = OsType.getLocal() == OsType.WINDOWS ? ".exe" : ""; + return sc.view().fileExists(getBinaryDirectory(sc).join("starship" + extension)); } @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(); + if (sc.getShellDialect() == ShellDialects.CMD) { + ClinkHelper.install(sc); + var configDir = getConfigurationDirectory(sc); + sc.view().mkdir(configDir); + sc.view().writeTextFile(configDir.join("starship.lua"), "load(io.popen('starship init cmd'):read(\"*a\"))()"); + } + + var dir = getBinaryDirectory(sc); + sc.view().mkdir(dir); + if (sc.getOsType() == OsType.WINDOWS) { + var file = GithubReleaseDownloader.getDownloadTempFile("starship/starship", + "starship-x86_64-pc-windows-msvc.zip", + s -> s.equals("starship-x86_64-pc-windows-msvc.zip")); + try (var fs = FileSystems.newFileSystem(file)) { + var exeFile = fs.getPath("starship.exe"); + sc.view().transferLocalFile(exeFile, dir.join("starship.exe")); + } + } else { + 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"); + var file = getConfigurationDirectory(sc).join("starship.toml"); sc.view().writeTextFile(file, configuration); return file; } @@ -73,25 +96,26 @@ public class StarshipTerminalPrompt extends ConfigFileTerminalPrompt { } @Override - public ShellTerminalInitCommand terminalCommand(ShellControl shellControl, FilePath configFile) throws Exception { - return new ShellTerminalInitCommand() { - @Override - public Optional 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; - } - - - }; + protected ShellScript setupTerminalCommand(ShellControl shellControl, FilePath config) throws Exception { + var lines = new ArrayList(); + if (shellControl.getShellDialect() == ShellDialects.CMD) { + lines.add(shellControl.getShellDialect().addToPathVariableCommand(List.of(ClinkHelper.getTargetDir(shellControl).toString()), false)); + } + lines.add(shellControl.getShellDialect().getSetEnvironmentVariableCommand("STARSHIP_CONFIG", config.toString())); + if (shellControl.getShellDialect() == ShellDialects.CMD) { + lines.add("clink inject --quiet --profile \"" + getConfigurationDirectory(shellControl) + "\""); + } else if (ShellDialects.isPowershell(shellControl)) { + lines.add("Invoke-Expression (&starship init powershell)"); + } else if (shellControl.getShellDialect() == ShellDialects.FISH) { + lines.add("starship init fish | source"); + } else { + lines.add("eval \"$(starship init " + shellControl.getShellDialect().getId() + ")\""); + } + return ShellScript.lines(lines); } @Override public List getSupportedDialects() { - return List.of(ShellDialects.BASH); + return List.of(ShellDialects.BASH, ShellDialects.ZSH, ShellDialects.FISH, ShellDialects.CMD, ShellDialects.POWERSHELL, ShellDialects.POWERSHELL_CORE); } } diff --git a/app/src/main/java/io/xpipe/app/terminal/TerminalPrompt.java b/app/src/main/java/io/xpipe/app/terminal/TerminalPrompt.java index b8d9c137c..41ebd5bae 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalPrompt.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalPrompt.java @@ -1,6 +1,7 @@ package io.xpipe.app.terminal; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.util.ShellTemp; import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellDialect; @@ -22,18 +23,31 @@ public interface TerminalPrompt { String getDocsLink(); default FilePath getConfigurationDirectory(ShellControl sc) throws Exception { - return ShellTemp.createUserSpecificTempDataDirectory(sc, "prompt"); + var d = ShellTemp.createUserSpecificTempDataDirectory(sc, "prompt").join(getId()); + sc.view().mkdir(d); + return d; } default FilePath getBinaryDirectory(ShellControl sc) throws Exception { - return ShellTemp.createUserSpecificTempDataDirectory(sc, "bin"); + var d = ShellTemp.createUserSpecificTempDataDirectory(sc, "bin").join(getId()); + sc.view().mkdir(d); + return d; } - default void installIfNeeded(ShellControl sc) throws Exception { - if (checkIfInstalled(sc)) { - checkCanInstall(sc); - install(sc); + String getId(); + + default boolean installIfNeeded(ShellControl sc) throws Exception { + if (!checkIfInstalled(sc)) { + try { + checkCanInstall(sc); + install(sc); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).omit().handle(); + return false; + } + return true; } + return true; } void checkCanInstall(ShellControl sc) throws Exception; @@ -42,7 +56,7 @@ public interface TerminalPrompt { void install(ShellControl sc) throws Exception; - ShellTerminalInitCommand terminalCommand(ShellControl shellControl) throws Exception; + ShellTerminalInitCommand terminalCommand() throws Exception; List getSupportedDialects(); } diff --git a/app/src/main/java/io/xpipe/app/terminal/TerminalPromptManager.java b/app/src/main/java/io/xpipe/app/terminal/TerminalPromptManager.java index d8601f878..eb02e52fa 100644 --- a/app/src/main/java/io/xpipe/app/terminal/TerminalPromptManager.java +++ b/app/src/main/java/io/xpipe/app/terminal/TerminalPromptManager.java @@ -12,14 +12,8 @@ public class TerminalPromptManager { return; } - var d = p.getSupportedDialects(); - if (!d.contains(sc.getShellDialect())) { - return; - } - try { - p.installIfNeeded(sc); - sc.withInitSnippet(p.terminalCommand(sc)); + sc.withInitSnippet(p.terminalCommand()); } catch (Exception e) { ErrorEvent.fromThrowable(e).handle(); } diff --git a/app/src/main/java/io/xpipe/app/util/GithubReleaseDownloader.java b/app/src/main/java/io/xpipe/app/util/GithubReleaseDownloader.java new file mode 100644 index 000000000..9357df2f5 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/GithubReleaseDownloader.java @@ -0,0 +1,63 @@ +package io.xpipe.app.util; + +import io.xpipe.core.process.ShellControl; +import io.xpipe.core.store.FilePath; +import io.xpipe.core.util.JacksonMapper; +import lombok.Value; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Predicate; + +public class GithubReleaseDownloader { + + public static Path getDownloadTempFile(String repository, String id, Predicate filter) throws Exception { + var tempDir = ShellTemp.getLocalTempDataDirectory("github"); + var temp = tempDir.resolve(id); + if (Files.exists(temp)) { + return temp; + } + + var request = HttpRequest.newBuilder().GET().uri(URI.create(getDownloadUrl(repository, filter))).build(); + var r = HttpHelper.client().send(request, + HttpResponse.BodyHandlers.ofByteArray()); + if (r.statusCode() >= 400) { + throw new IOException(new String(r.body(), StandardCharsets.UTF_8)); + } + + Files.createDirectories(tempDir); + Files.write(temp, r.body()); + return temp; + } + + private static String getDownloadUrl(String repository, Predicate filter) throws Exception { + var request = HttpRequest.newBuilder().GET().uri(URI.create("https://api.github.com/repos/" + repository + "/releases")).build(); + var r = HttpHelper.client().send(request, + HttpResponse.BodyHandlers.ofString()); + if (r.statusCode() >= 400) { + throw new IOException(r.body()); + } + + var json = JacksonMapper.getDefault().readTree(r.body()); + var latest = json.get(0); + var assets = latest.required("assets"); + for (var asset : assets) { + var name = asset.required("name").asText(); + if (filter.test(name)) { + var url = asset.required("browser_download_url").asText(); + return url; + } + } + + throw new IllegalStateException("Unable to find download url for " + repository); + } +} diff --git a/app/src/main/java/io/xpipe/app/util/ShellTemp.java b/app/src/main/java/io/xpipe/app/util/ShellTemp.java index 91a26cedf..c19874591 100644 --- a/app/src/main/java/io/xpipe/app/util/ShellTemp.java +++ b/app/src/main/java/io/xpipe/app/util/ShellTemp.java @@ -49,7 +49,7 @@ public class ShellTemp { proc.command("chmod 777 " + proc.getShellDialect().fileArgument(base)) .executeAndCheck(); var user = proc.getShellDialect().printUsernameCommand(proc).readStdoutOrThrow(); - base = temp.join(user); + base = base.join(user); } else { var temp = proc.getSystemTemporaryDirectory(); base = temp.join("xpipe"); diff --git a/app/src/main/java/io/xpipe/app/util/SimpleFilterInputStream.java b/app/src/main/java/io/xpipe/app/util/SimpleFilterInputStream.java index ce74a3067..e087690eb 100644 --- a/app/src/main/java/io/xpipe/app/util/SimpleFilterInputStream.java +++ b/app/src/main/java/io/xpipe/app/util/SimpleFilterInputStream.java @@ -18,12 +18,12 @@ public abstract class SimpleFilterInputStream extends FilterInputStream { @Override public int read(byte @NonNull [] b, int off, int len) throws IOException { for (int i = off; i < off + len; i++) { - var r = (byte) read(); + var r = read(); if (r == -1) { return i - off == 0 ? -1 : i - off; } - b[i] = r; + b[i] = (byte) r; } return len; } diff --git a/core/src/main/java/io/xpipe/core/process/ShellScript.java b/core/src/main/java/io/xpipe/core/process/ShellScript.java index 122a0b0de..d8bd8fb84 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellScript.java +++ b/core/src/main/java/io/xpipe/core/process/ShellScript.java @@ -3,6 +3,7 @@ package io.xpipe.core.process; import lombok.Value; import java.util.Arrays; +import java.util.List; import java.util.stream.Collectors; @Value @@ -12,6 +13,10 @@ public class ShellScript { return new ShellScript(Arrays.stream(lines).collect(Collectors.joining("\n"))); } + public static ShellScript lines(List lines) { + return new ShellScript(lines.stream().collect(Collectors.joining("\n"))); + } + String value; @Override diff --git a/core/src/main/java/io/xpipe/core/process/ShellView.java b/core/src/main/java/io/xpipe/core/process/ShellView.java index 36d874228..be5b1b374 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellView.java +++ b/core/src/main/java/io/xpipe/core/process/ShellView.java @@ -3,6 +3,8 @@ package io.xpipe.core.process; import io.xpipe.core.store.FilePath; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Optional; public class ShellView { @@ -66,6 +68,12 @@ public class ShellView { .executeAndCheck(); } + public void mkdir(FilePath path) throws Exception { + shellControl.command(getDialect() + .getMkdirsCommand(path.toString())) + .execute(); + } + public boolean directoryExists(FilePath path) throws Exception { return getDialect().directoryExists(shellControl, path.toString()).executeAndCheck(); } @@ -104,6 +112,12 @@ public class ShellView { return out.flatMap(s -> s.lines().findFirst()).map(String::trim).map(s -> FilePath.of(s)); } + public void transferLocalFile(Path localPath, FilePath target) throws Exception { + try (var in = Files.newInputStream(localPath)) { + writeStreamFile(target, in, in.available()); + } + } + public boolean isInPath(String executable) throws Exception { return shellControl.executeSimpleBooleanCommand( shellControl.getShellDialect().getWhichCommand(executable)); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStoreSetup.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStoreSetup.java index f0fc1cecb..74d1f48e6 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStoreSetup.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStoreSetup.java @@ -15,16 +15,16 @@ import java.util.*; public class ScriptStoreSetup { - public static ShellControl controlWithDefaultScripts(ShellControl pc) { - return controlWithScripts(pc, getEnabledScripts()); + public static void controlWithDefaultScripts(ShellControl pc) { + controlWithScripts(pc, getEnabledScripts()); } - public static ShellControl controlWithScripts( + public static void controlWithScripts( ShellControl pc, List> enabledScripts) { try { // Don't copy scripts if we don't want to modify the file system if (!pc.getEffectiveSecurityPolicy().permitTempScriptCreation()) { - return pc; + return; } var initFlattened = flatten(enabledScripts).stream() @@ -36,7 +36,7 @@ public class ScriptStoreSetup { // Optimize if we have nothing to do if (initFlattened.isEmpty() && bringFlattened.isEmpty()) { - return pc; + return; } initFlattened.forEach(s -> { @@ -77,7 +77,6 @@ public class ScriptStoreSetup { } }); } - return pc; } catch (StackOverflowError t) { throw ErrorEvent.expected( new RuntimeException("Unable to set up scripts. Is there a circular script dependency?", t)); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/store/ShellStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/store/ShellStoreProvider.java index f907e2991..09f5e9a9d 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/store/ShellStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/store/ShellStoreProvider.java @@ -10,6 +10,8 @@ import io.xpipe.app.ext.*; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.terminal.TerminalLauncher; +import io.xpipe.app.terminal.TerminalPromptManager; +import io.xpipe.app.terminal.TerminalProxyManager; import io.xpipe.app.util.ShellStoreFormat; import io.xpipe.ext.base.script.ScriptStoreSetup; @@ -25,7 +27,9 @@ public interface ShellStoreProvider extends DataStoreProvider { public void execute() throws Exception { var replacement = ProcessControlProvider.get().replace(entry.ref()); ShellStore store = replacement.getStore().asNeeded(); - var control = ScriptStoreSetup.controlWithDefaultScripts(store.standaloneControl()); + var control = store.standaloneControl(); + ScriptStoreSetup.controlWithDefaultScripts(control); + TerminalPromptManager.configurePromptScript(control); TerminalLauncher.open( replacement.get(), DataStorage.get().getStoreEntryDisplayName(replacement.get()), diff --git a/lang/strings/translations_en.properties b/lang/strings/translations_en.properties index a815d9d7c..cd5fcf942 100644 --- a/lang/strings/translations_en.properties +++ b/lang/strings/translations_en.properties @@ -1381,4 +1381,7 @@ refreshOpenpubkey=Refresh openpubkey identity refreshOpenpubkeyDescription=Run opkssh refresh to make the openpubkey identity valid again all=All terminalPrompt=Terminal prompt +terminalPromptDescription=The terminal prompt tool to use in your remote terminals.\n\nEnabling a terminal prompt will automatically set up and configure the prompt tool on the target system when opening a terminal session. This will increase the terminal loading time for the first time while the prompt is being set up on the remote system. terminalPromptConfiguration=Terminal prompt configuration +terminalPromptConfig=Config file +terminalPromptConfigDescription=The custom config file to apply to the prompt. This config will be automatically set up on the target system when the terminal is initialized and used as the default prompt config.\n\nIf you want to use the existing default config file on each system, you can leave this field empty.