diff --git a/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java b/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java index c9d0cabb2..99e8812d8 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java @@ -33,26 +33,49 @@ public class SystemStateComp extends SimpleComp { PlatformThread.runLaterIfNeeded(() -> fi.setIconLiteral(i)); }); - var border = new FontIcon("mdi2c-circle-outline"); + var bg = new FontIcon("mdi2s-square-rounded"); + bg.getStyleClass().add("background-icon"); + + var border = new FontIcon("mdi2s-square-rounded-outline"); border.getStyleClass().add("outer-icon"); - border.setOpacity(0.5); + border.setOpacity(0.3); var success = Styles.toDataURI( - ".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-success-emphasis; }"); + """ + .stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-success-emphasis; } + + .stacked-ikonli-font-icon > .background-icon { -fx-icon-color: -color-success-9; } + """ + ); var failure = - Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-danger-emphasis; }"); + Styles.toDataURI( + """ + .stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-danger-emphasis; } + + .stacked-ikonli-font-icon > .background-icon { -fx-icon-color: -color-danger-9; } + """ + ); var other = - Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-accent-emphasis; }"); + Styles.toDataURI( + """ + .stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-accent-emphasis; } + + .stacked-ikonli-font-icon > .background-icon { -fx-icon-color: -color-accent-9; } + """ + ); var pane = new StackedFontIcon(); - pane.getChildren().addAll(fi, border); + pane.getChildren().addAll(bg, fi, border); pane.setAlignment(Pos.CENTER); var dataClass1 = """ .stacked-ikonli-font-icon > .outer-icon { - -fx-icon-size: 22px; + -fx-icon-size: 26px; } + .stacked-ikonli-font-icon > .background-icon { + -fx-icon-size: 26px; + } .stacked-ikonli-font-icon > .inner-icon { -fx-icon-size: 12px; } diff --git a/app/src/main/java/io/xpipe/app/comp/base/TtyWarningComp.java b/app/src/main/java/io/xpipe/app/comp/base/TtyWarningComp.java new file mode 100644 index 000000000..34e760b4e --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/base/TtyWarningComp.java @@ -0,0 +1,50 @@ +package io.xpipe.app.comp.base; + +import atlantafx.base.theme.Styles; +import io.xpipe.app.fxcomps.SimpleComp; +import io.xpipe.app.fxcomps.impl.TooltipAugment; +import javafx.geometry.Pos; +import javafx.scene.layout.Region; +import lombok.Getter; +import org.kordamp.ikonli.javafx.FontIcon; +import org.kordamp.ikonli.javafx.StackedFontIcon; + +@Getter +public class TtyWarningComp extends SimpleComp { + + @Override + protected Region createSimple() { + var fi = new FontIcon("mdi2l-lightning-bolt"); + fi.getStyleClass().add("inner-icon"); + + var border = new FontIcon("mdi2s-square-rounded-outline"); + border.getStyleClass().add("outer-icon"); + border.setOpacity(0.5); + + var bg = new FontIcon("mdi2s-square-rounded"); + bg.getStyleClass().add("background-icon"); + + var pane = new StackedFontIcon(); + pane.getChildren().addAll(bg, fi, border); + pane.setAlignment(Pos.CENTER); + + var style = + """ + .stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-danger-emphasis; } + + .stacked-ikonli-font-icon > .outer-icon { + -fx-icon-size: 26px; + } + .stacked-ikonli-font-icon > .background-icon { + -fx-icon-size: 26px; + -fx-icon-color: -color-danger-9; + } + .stacked-ikonli-font-icon > .inner-icon { + -fx-icon-size: 12px; + } + """; + pane.getStylesheets().add(Styles.toDataURI(style)); + new TooltipAugment<>("ttyWarning", null).augment(pane); + return pane; + } +} diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/TooltipAugment.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/TooltipAugment.java index e7c7aeabf..67d820806 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/TooltipAugment.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/TooltipAugment.java @@ -10,6 +10,7 @@ import javafx.beans.value.ObservableValue; import javafx.scene.control.Tooltip; import javafx.scene.input.KeyCombination; import javafx.stage.Window; +import javafx.util.Duration; public class TooltipAugment> implements Augment { @@ -45,6 +46,7 @@ public class TooltipAugment> implements Augment { tt.setWrapText(true); tt.setMaxWidth(400); tt.getStyleClass().add("fancy-tooltip"); + tt.setHideDelay(Duration.INDEFINITE); Tooltip.install(struc.get(), tt); } diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java index cafa708a7..c54aaff58 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java @@ -109,6 +109,9 @@ public class AppPrefs { map(new SimpleBooleanProperty(false), "developerDisableGuiRestrictions", Boolean.class); private final ObservableBooleanValue developerDisableGuiRestrictionsEffective = bindDeveloperTrue(developerDisableGuiRestrictions); + final BooleanProperty developerForceSshTty = + map(new SimpleBooleanProperty(false), "developerForceSshTty", Boolean.class); + final ObjectProperty language = map(new SimpleObjectProperty<>(SupportedLocale.getEnglish()), "language", SupportedLocale.class); @@ -435,6 +438,10 @@ public class AppPrefs { return developerDisableGuiRestrictionsEffective; } + public ObservableBooleanValue developerForceSshTty() { + return bindDeveloperTrue(developerForceSshTty); + } + @SuppressWarnings("unchecked") private T map(T o, String name, Class clazz) { mapping.add(new Mapping<>(name, (Property) o, (Class) clazz)); diff --git a/app/src/main/java/io/xpipe/app/prefs/DeveloperCategory.java b/app/src/main/java/io/xpipe/app/prefs/DeveloperCategory.java index 3876d4cf4..11dde276b 100644 --- a/app/src/main/java/io/xpipe/app/prefs/DeveloperCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/DeveloperCategory.java @@ -61,6 +61,8 @@ public class DeveloperCategory extends AppPrefsCategory { .sub(new OptionsBuilder() .nameAndDescription("developerDisableUpdateVersionCheck") .addToggle(prefs.developerDisableUpdateVersionCheck) + .nameAndDescription("developerForceSshTty") + .addToggle(prefs.developerForceSshTty) .nameAndDescription("developerDisableGuiRestrictions") .addToggle(prefs.developerDisableGuiRestrictions) .nameAndDescription("shellCommandTest") diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java index 82153394e..c32dd4254 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java @@ -114,14 +114,12 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue { protected Optional determineFromPath() { // Try to locate if it is in the Path - try (var cc = LocalShell.getShell() - .command(CommandBuilder.ofFunction( - var1 -> var1.getShellDialect().getWhichCommand(executable))) + try (var sc = LocalShell.getShell() .start()) { - var out = cc.readStdoutDiscardErr(); - var exit = cc.getExitCode(); - if (exit == 0) { - var first = out.lines().findFirst(); + var out = sc.command(CommandBuilder.ofFunction( + var1 -> var1.getShellDialect().getWhichCommand(executable))).readStdoutIfPossible(); + if (out.isPresent()) { + var first = out.get().lines().findFirst(); if (first.isPresent()) { return first.map(String::trim).map(Path::of); } diff --git a/app/src/main/java/io/xpipe/app/update/XPipeDistributionType.java b/app/src/main/java/io/xpipe/app/update/XPipeDistributionType.java index 410aa38a1..0d6c8d6e3 100644 --- a/app/src/main/java/io/xpipe/app/update/XPipeDistributionType.java +++ b/app/src/main/java/io/xpipe/app/update/XPipeDistributionType.java @@ -99,16 +99,13 @@ public enum XPipeDistributionType { // In theory, we can also add && !AppProperties.get().isStaging() here, but we want to replicate the // production behavior if (OsType.getLocal().equals(OsType.WINDOWS)) { - try (var chocoOut = - sc.command("choco search --local-only -r xpipe").start()) { - var out = chocoOut.readStdoutDiscardErr(); - if (chocoOut.getExitCode() == 0) { - var split = out.split("\\|"); - if (split.length == 2) { - var version = split[1]; - if (AppProperties.get().getVersion().equals(version)) { - return CHOCO; - } + var out = sc.command("choco search --local-only -r xpipe").readStdoutIfPossible(); + if (out.isPresent()) { + var split = out.get().split("\\|"); + if (split.length == 2) { + var version = split[1]; + if (AppProperties.get().getVersion().equals(version)) { + return CHOCO; } } } @@ -117,17 +114,15 @@ public enum XPipeDistributionType { // In theory, we can also add && !AppProperties.get().isStaging() here, but we want to replicate the // production behavior if (OsType.getLocal().equals(OsType.MACOS)) { - try (var brewOut = sc.command("brew list --casks --versions").start()) { - var out = brewOut.readStdoutDiscardErr(); - if (brewOut.getExitCode() == 0) { - if (out.lines().anyMatch(s -> { - var split = s.split(" "); - return split.length == 2 - && split[0].equals("xpipe") - && split[1].equals(AppProperties.get().getVersion()); - })) { - return HOMEBREW; - } + var out = sc.command("brew list --casks --versions").readStdoutIfPossible(); + if (out.isPresent()) { + if (out.get().lines().anyMatch(s -> { + var split = s.split(" "); + return split.length == 2 + && split[0].equals("xpipe") + && split[1].equals(AppProperties.get().getVersion()); + })) { + return HOMEBREW; } } } diff --git a/app/src/main/java/io/xpipe/app/util/DesktopHelper.java b/app/src/main/java/io/xpipe/app/util/DesktopHelper.java index 9185e57ba..190c20088 100644 --- a/app/src/main/java/io/xpipe/app/util/DesktopHelper.java +++ b/app/src/main/java/io/xpipe/app/util/DesktopHelper.java @@ -16,11 +16,10 @@ public class DesktopHelper { return Path.of(LocalShell.getLocalPowershell() .executeSimpleStringCommand("[Environment]::GetFolderPath([Environment+SpecialFolder]::Desktop)")); } else if (OsType.getLocal() == OsType.LINUX) { - try (var cmd = LocalShell.getShell().command("xdg-user-dir DESKTOP").start()) { - var read = cmd.readStdoutDiscardErr(); - var exit = cmd.getExitCode(); - if (exit == 0) { - return Path.of(read); + try (var sc = LocalShell.getShell().start()) { + var out = sc.command("xdg-user-dir DESKTOP").readStdoutIfPossible(); + if (out.isPresent()) { + return Path.of(out.get()); } } } @@ -34,12 +33,10 @@ public class DesktopHelper { .executeSimpleStringCommand( "(New-Object -ComObject Shell.Application).NameSpace('shell:Downloads').Self.Path")); } else if (OsType.getLocal() == OsType.LINUX) { - try (var cmd = - LocalShell.getShell().command("xdg-user-dir DOWNLOAD").start()) { - var read = cmd.readStdoutDiscardErr(); - var exit = cmd.getExitCode(); - if (exit == 0) { - return Path.of(read); + try (var sc = LocalShell.getShell().start()) { + var out = sc.command("xdg-user-dir DOWNLOAD").readStdoutIfPossible(); + if (out.isPresent()) { + return Path.of(out.get()); } } } diff --git a/app/src/main/java/io/xpipe/app/util/WindowsRegistry.java b/app/src/main/java/io/xpipe/app/util/WindowsRegistry.java index ce952d374..a2aca1d11 100644 --- a/app/src/main/java/io/xpipe/app/util/WindowsRegistry.java +++ b/app/src/main/java/io/xpipe/app/util/WindowsRegistry.java @@ -145,23 +145,20 @@ public abstract class WindowsRegistry { .add("/v") .addQuoted(valueName); - String output; - try (var c = shellControl.command(command).start()) { - output = c.readStdoutDiscardErr(); - if (c.getExitCode() != 0) { - return Optional.empty(); - } + var output = shellControl.command(command).readStdoutIfPossible(); + if (output.isEmpty()) { + return Optional.empty(); } // Output has the following format: // \n\n\n\t\t - if (output.contains("\t")) { - String[] parsed = output.split("\t"); + if (output.get().contains("\t")) { + String[] parsed = output.get().split("\t"); return Optional.of(parsed[parsed.length - 1]); } - if (output.contains(" ")) { - String[] parsed = output.split(" "); + if (output.get().contains(" ")) { + String[] parsed = output.get().split(" "); return Optional.of(parsed[parsed.length - 1]); } @@ -176,14 +173,7 @@ public abstract class WindowsRegistry { .add("/v") .addQuoted(valueName) .add("/s"); - try (var c = shellControl.command(command).start()) { - var output = c.readStdoutDiscardErr(); - if (c.getExitCode() != 0) { - return Optional.empty(); - } else { - return Optional.of(output); - } - } + return shellControl.command(command).readStdoutIfPossible(); } @Override @@ -196,22 +186,17 @@ public abstract class WindowsRegistry { .add("/s") .add("/e") .add("/d"); - try (var c = shellControl.command(command).start()) { - var output = c.readStdoutDiscardErr(); - if (c.getExitCode() != 0) { + return shellControl.command(command).readStdoutIfPossible().flatMap(output -> { + return output.lines().findFirst().flatMap(s -> { + if (s.startsWith("HKEY_CURRENT_USER\\")) { + return Optional.of(new Key(HKEY_CURRENT_USER, s.replace("HKEY_CURRENT_USER\\", ""))); + } + if (s.startsWith("HKEY_LOCAL_MACHINE\\")) { + return Optional.of(new Key(HKEY_LOCAL_MACHINE, s.replace("HKEY_LOCAL_MACHINE\\", ""))); + } return Optional.empty(); - } else { - return output.lines().findFirst().flatMap(s -> { - if (s.startsWith("HKEY_CURRENT_USER\\")) { - return Optional.of(new Key(HKEY_CURRENT_USER, s.replace("HKEY_CURRENT_USER\\", ""))); - } - if (s.startsWith("HKEY_LOCAL_MACHINE\\")) { - return Optional.of(new Key(HKEY_LOCAL_MACHINE, s.replace("HKEY_LOCAL_MACHINE\\", ""))); - } - return Optional.empty(); - }); - } - } + }); + }); } } } diff --git a/app/src/main/resources/io/xpipe/app/resources/misc/welcome.md b/app/src/main/resources/io/xpipe/app/resources/misc/welcome.md index 3d99b6073..9b4c4dc32 100644 --- a/app/src/main/resources/io/xpipe/app/resources/misc/welcome.md +++ b/app/src/main/resources/io/xpipe/app/resources/misc/welcome.md @@ -1,11 +1,10 @@ ## Welcome -Thank you for using XPipe! +Welcome to XPipe! + You can view the development status, report issues, and more at the following places: - [GitHub Repository](https://github.com/xpipe-io/xpipe/) - [Discord Server](https://discord.gg/8y89vS8cRb) - [Slack Server](https://join.slack.com/t/XPipe/shared_invite/zt-1awjq0t5j-5i4UjNJfNe1VN4b_auu6Cg) -- [Email me](mailto://crschnick@xpipe.io) - -Note that the XPipe project currently is a one-man show, but I still try to respond to everything in time. +- [Email us](mailto://hello@xpipe.io) diff --git a/app/src/main/resources/io/xpipe/app/resources/style/style.css b/app/src/main/resources/io/xpipe/app/resources/style/style.css index 7a27b0317..747928eb2 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/style.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/style.css @@ -128,3 +128,7 @@ .root:dark .loading-comp { -fx-background-color: rgba(0, 0, 0, 0.5); } + +.root:light .stacked-ikonli-font-icon > .background-icon { -fx-opacity: 0; } + +.stacked-ikonli-font-icon > .background-icon { -fx-opacity: 0.2; } diff --git a/core/src/main/java/io/xpipe/core/process/CommandControl.java b/core/src/main/java/io/xpipe/core/process/CommandControl.java index 1e4dc6070..2e76586e0 100644 --- a/core/src/main/java/io/xpipe/core/process/CommandControl.java +++ b/core/src/main/java/io/xpipe/core/process/CommandControl.java @@ -1,16 +1,10 @@ package io.xpipe.core.process; -import io.xpipe.core.util.FailableConsumer; - -import com.fasterxml.jackson.databind.JsonNode; - import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; import java.nio.charset.Charset; import java.time.Duration; import java.util.Optional; -import java.util.function.Consumer; import java.util.function.Function; public interface CommandControl extends ProcessControl { @@ -70,32 +64,14 @@ public interface CommandControl extends ProcessControl { CommandControl elevated(ElevationFunction function); - void withStdoutOrThrow(FailableConsumer c); - String[] readStdoutAndStderr() throws Exception; - String readStdoutDiscardErr() throws Exception; - - String readStderrDiscardStdout() throws Exception; - void discardOrThrow() throws Exception; - void accumulateStdout(Consumer con); - - void accumulateStderr(Consumer con); - byte[] readRawBytesOrThrow() throws Exception; String readStdoutOrThrow() throws Exception; - JsonNode readStdoutJsonOrThrow() throws Exception; - - String readStderrOrThrow() throws Exception; - - String readStdoutAndWait() throws Exception; - - String readStderrAndWait() throws Exception; - Optional readStdoutIfPossible() throws Exception; default boolean discardAndCheckExit() throws ProcessOutputException { @@ -113,10 +89,6 @@ public interface CommandControl extends ProcessControl { } } - void discardOut(); - - void discardErr(); - enum TerminalExitMode { KEEP_OPEN, CLOSE diff --git a/core/src/main/java/io/xpipe/core/process/CommandFeedbackPredicate.java b/core/src/main/java/io/xpipe/core/process/CommandFeedbackPredicate.java deleted file mode 100644 index 24d569580..000000000 --- a/core/src/main/java/io/xpipe/core/process/CommandFeedbackPredicate.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.xpipe.core.process; - -public interface CommandFeedbackPredicate { - - boolean test(CommandBuilder command); -} diff --git a/core/src/main/java/io/xpipe/core/process/OsType.java b/core/src/main/java/io/xpipe/core/process/OsType.java index 8b943fca7..12b272cd0 100644 --- a/core/src/main/java/io/xpipe/core/process/OsType.java +++ b/core/src/main/java/io/xpipe/core/process/OsType.java @@ -181,19 +181,15 @@ public interface OsType { @Override public String determineOperatingSystemName(ShellControl pc) throws Exception { String type = "Unknown"; - try (CommandControl c = pc.command("uname -o").start()) { - var text = c.readStdoutDiscardErr(); - if (c.getExitCode() == 0) { - type = text.strip(); - } + var uname = pc.command("uname -o").readStdoutIfPossible(); + if (uname.isPresent()) { + type = uname.get(); } String version = "?"; - try (CommandControl c = pc.command("uname -r").start()) { - var text = c.readStdoutDiscardErr(); - if (c.getExitCode() == 0) { - version = text.strip(); - } + var unameR = pc.command("uname -r").readStdoutIfPossible(); + if (unameR.isPresent()) { + version = unameR.get(); } return type + " " + version; @@ -209,18 +205,14 @@ public interface OsType { @Override public String determineOperatingSystemName(ShellControl pc) throws Exception { - try (CommandControl c = pc.command("lsb_release -a").start()) { - var text = c.readStdoutDiscardErr(); - if (c.getExitCode() == 0) { - return PropertiesFormatsParser.parse(text, ":").getOrDefault("Description", "Unknown"); - } + var rel = pc.command("lsb_release -a").readStdoutIfPossible(); + if (rel.isPresent()) { + return PropertiesFormatsParser.parse(rel.get(), ":").getOrDefault("Description", "Unknown"); } - try (CommandControl c = pc.command("cat /etc/*release").start()) { - var text = c.readStdoutDiscardErr(); - if (c.getExitCode() == 0) { - return PropertiesFormatsParser.parse(text, "=").getOrDefault("PRETTY_NAME", "Unknown"); - } + var cat = pc.command("cat /etc/*release").readStdoutIfPossible(); + if (cat.isPresent()) { + return PropertiesFormatsParser.parse(cat.get(), "=").getOrDefault("PRETTY_NAME", "Unknown"); } return super.determineOperatingSystemName(pc); diff --git a/core/src/main/java/io/xpipe/core/process/ShellControl.java b/core/src/main/java/io/xpipe/core/process/ShellControl.java index 3c1efe058..8c274344c 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellControl.java +++ b/core/src/main/java/io/xpipe/core/process/ShellControl.java @@ -18,6 +18,8 @@ import java.util.function.Function; public interface ShellControl extends ProcessControl { + ShellTtyState getTtyState(); + void setNonInteractive(); boolean isInteractive(); @@ -65,6 +67,7 @@ public interface ShellControl extends ProcessControl { var s = store.getState().toBuilder() .osType(shellControl.getOsType()) .shellDialect(shellControl.getOriginalShellDialect()) + .ttyState(shellControl.getTtyState()) .running(true) .osName(shellControl.getOsName()) .build(); @@ -113,25 +116,12 @@ public interface ShellControl extends ProcessControl { script)); } - default byte[] executeSimpleRawBytesCommand(String command) throws Exception { - try (CommandControl c = command(command).start()) { - return c.readRawBytesOrThrow(); - } - } - default String executeSimpleStringCommand(String command) throws Exception { try (CommandControl c = command(command).start()) { return c.readStdoutOrThrow(); } } - default Optional executeSimpleStringCommandAndCheck(String command) throws Exception { - try (CommandControl c = command(command).start()) { - var out = c.readStdoutDiscardErr(); - return c.getExitCode() == 0 ? Optional.of(out) : Optional.empty(); - } - } - default boolean executeSimpleBooleanCommand(String command) throws Exception { try (CommandControl c = command(command).start()) { return c.discardAndCheckExit(); @@ -150,20 +140,6 @@ public interface ShellControl extends ProcessControl { } } - default void executeSimpleCommand(String command, String failMessage) throws Exception { - try (CommandControl c = command(command).start()) { - c.discardOrThrow(); - } catch (ProcessOutputException out) { - throw ProcessOutputException.withPrefix(failMessage, out); - } - } - - default String executeSimpleStringCommand(ShellDialect type, String command) throws Exception { - try (var sub = subShell(type).start()) { - return sub.executeSimpleStringCommand(command); - } - } - ShellControl withSecurityPolicy(ShellSecurityPolicy policy); ShellSecurityPolicy getEffectiveSecurityPolicy(); diff --git a/core/src/main/java/io/xpipe/core/process/ShellDialect.java b/core/src/main/java/io/xpipe/core/process/ShellDialect.java index 880e45000..8c19b108e 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellDialect.java +++ b/core/src/main/java/io/xpipe/core/process/ShellDialect.java @@ -184,5 +184,5 @@ public interface ShellDialect { String getDisplayName(); - boolean doesEchoInput(); + boolean doesEchoInputByDefault(); } diff --git a/core/src/main/java/io/xpipe/core/process/ShellProperties.java b/core/src/main/java/io/xpipe/core/process/ShellProperties.java deleted file mode 100644 index b8df6d34d..000000000 --- a/core/src/main/java/io/xpipe/core/process/ShellProperties.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.xpipe.core.process; - -import lombok.Value; - -@Value -public class ShellProperties { - - ShellDialect dialect; - boolean ansiEscapes; -} diff --git a/core/src/main/java/io/xpipe/core/process/ShellStoreState.java b/core/src/main/java/io/xpipe/core/process/ShellStoreState.java index 72dcc9efe..4f2d1360f 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellStoreState.java +++ b/core/src/main/java/io/xpipe/core/process/ShellStoreState.java @@ -19,6 +19,7 @@ public class ShellStoreState extends DataStoreState implements OsNameState { OsType.Any osType; String osName; ShellDialect shellDialect; + ShellTtyState ttyState; Boolean running; public boolean isRunning() { @@ -39,6 +40,7 @@ public class ShellStoreState extends DataStoreState implements OsNameState { b.osType(useNewer(osType, shellStoreState.getOsType())) .osName(useNewer(osName, shellStoreState.getOsName())) .shellDialect(useNewer(shellDialect, shellStoreState.getShellDialect())) + .ttyState(useNewer(ttyState, shellStoreState.getTtyState())) .running(useNewer(running, shellStoreState.getRunning())); } } diff --git a/core/src/main/java/io/xpipe/core/process/ShellTtyState.java b/core/src/main/java/io/xpipe/core/process/ShellTtyState.java new file mode 100644 index 000000000..d27745c57 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/process/ShellTtyState.java @@ -0,0 +1,25 @@ +package io.xpipe.core.process; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +@Getter +public enum ShellTtyState { + + @JsonProperty("none") + NONE(true, false, false), + @JsonProperty("merged") + MERGED_STDERR(false, false, false), + @JsonProperty("pty") + PTY_ALLOCATED(false, true, true); + + private final boolean hasSeparateStreams; + private final boolean hasAnsiEscapes; + private final boolean echoesAllInput; + + ShellTtyState(boolean hasSeparateStreams, boolean hasAnsiEscapes, boolean echoesAllInput) { + this.hasSeparateStreams = hasSeparateStreams; + this.hasAnsiEscapes = hasAnsiEscapes; + this.echoesAllInput = echoesAllInput; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/action/SampleStoreAction.java b/ext/base/src/main/java/io/xpipe/ext/base/action/SampleStoreAction.java index 27b2db2d5..77c8bc5eb 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/action/SampleStoreAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/action/SampleStoreAction.java @@ -10,13 +10,11 @@ import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellDialects; import io.xpipe.core.store.LocalStore; import io.xpipe.core.store.ShellStore; - import javafx.beans.value.ObservableValue; - import lombok.Value; import java.io.BufferedReader; -import java.io.InputStreamReader; +import java.io.StringReader; public class SampleStoreAction implements ActionProvider { @@ -83,23 +81,14 @@ public class SampleStoreAction implements ActionProvider { sc.executeSimpleStringCommand(sc.getShellDialect().getEchoCommand("hello!", false)); // You can also implement custom handling for more complex commands - try (CommandControl cc = sc.command("ls").start()) { - // Discard stderr - cc.discardErr(); - - // Read the stdout lines as a stream - BufferedReader reader = new BufferedReader(new InputStreamReader(cc.getStdout(), cc.getCharset())); - // We don't have to close this stream here, that will be automatically done by the command control - // after the try-with block - reader.lines().filter(s -> !s.isBlank()).forEach(s -> { - System.out.println(s); - }); - - // Waits for command completion and returns exit code - if (cc.getExitCode() != 0) { - // Handle failure - } - } + var lsOut = sc.command("ls").readStdoutOrThrow(); + // Read the stdout lines as a stream + BufferedReader reader = new BufferedReader(new StringReader(lsOut)); + // We don't have to close this stream here, that will be automatically done by the command control + // after the try-with block + reader.lines().filter(s -> !s.isBlank()).forEach(s -> { + System.out.println(s); + }); // Commands can also be more complex and span multiple lines. // In this case, XPipe will internally write a command to a script file and then execute the script diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/ToFileCommandAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/ToFileCommandAction.java index a9a1de88b..08001e17c 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/ToFileCommandAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/ToFileCommandAction.java @@ -16,12 +16,10 @@ public abstract class ToFileCommandAction implements LeafAction, ApplicationPath ShellControl sc = model.getFileSystem().getShell().orElseThrow(); for (BrowserEntry entry : entries) { var command = createCommand(model, entry); - try (var cc = sc.command(command) + var out = sc.command(command) .withWorkingDirectory(model.getCurrentDirectory().getPath()) - .start()) { - cc.discardErr(); - FileOpener.openCommandOutput(entry.getFileName(), entry, cc); - } + .readStdoutOrThrow(); + FileOpener.openReadOnlyString(out); } } 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 new file mode 100644 index 000000000..1ee97a740 --- /dev/null +++ b/ext/base/src/main/java/io/xpipe/ext/base/store/ShellStoreProvider.java @@ -0,0 +1,69 @@ +package io.xpipe.ext.base.store; + +import io.xpipe.app.browser.session.BrowserSessionModel; +import io.xpipe.app.comp.base.OsLogoComp; +import io.xpipe.app.comp.base.SystemStateComp; +import io.xpipe.app.comp.base.TtyWarningComp; +import io.xpipe.app.comp.store.StoreEntryComp; +import io.xpipe.app.comp.store.StoreEntryWrapper; +import io.xpipe.app.comp.store.StoreSection; +import io.xpipe.app.ext.ActionProvider; +import io.xpipe.app.ext.DataStoreProvider; +import io.xpipe.app.ext.DataStoreUsageCategory; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.util.TerminalLauncher; +import io.xpipe.core.process.ShellStoreState; +import io.xpipe.core.process.ShellTtyState; +import io.xpipe.core.store.ShellStore; +import io.xpipe.ext.base.script.ScriptStore; +import javafx.beans.binding.Bindings; +import javafx.beans.property.BooleanProperty; + +public interface ShellStoreProvider extends DataStoreProvider { + + default Comp createTtyWarning(StoreEntryWrapper w) { + return new TtyWarningComp().hide(Bindings.createObjectBinding( + () -> { + ShellStoreState state = (ShellStoreState) w.getPersistentState().getValue(); + return state.getTtyState() == ShellTtyState.NONE; + }, + w.getPersistentState())); + } + + @Override + default StoreEntryComp customEntryComp(StoreSection s, boolean preferLarge) { + return StoreEntryComp.create(s, createTtyWarning(s.getWrapper()), preferLarge); + } + + @Override + default ActionProvider.Action launchAction(DataStoreEntry entry) { + return new ActionProvider.Action() { + @Override + public void execute() throws Exception { + ShellStore store = entry.getStore().asNeeded(); + TerminalLauncher.open(entry, DataStorage.get().getStoreEntryDisplayName(entry), null, ScriptStore.controlWithDefaultScripts(store.control())); + } + }; + } + + @Override + default ActionProvider.Action browserAction(BrowserSessionModel sessionModel, DataStoreEntry store, BooleanProperty busy) { + return new ActionProvider.Action() { + @Override + public void execute() throws Exception { + sessionModel.openFileSystemAsync(store.ref(), null, busy); + } + }; + } + + default Comp stateDisplay(StoreEntryWrapper w) { + return new OsLogoComp(w, SystemStateComp.State.shellState(w)); + } + + @Override + default DataStoreUsageCategory getUsageCategory() { + return DataStoreUsageCategory.SHELL; + } +} diff --git a/lang/app/strings/translations_da.properties b/lang/app/strings/translations_da.properties index 2a94ff634..2d762ec2b 100644 --- a/lang/app/strings/translations_da.properties +++ b/lang/app/strings/translations_da.properties @@ -500,3 +500,6 @@ workspaceNameDescription=Visningsnavnet på arbejdsområdet workspacePath=Sti til arbejdsområde workspacePathDescription=Placeringen af arbejdsområdets datakatalog workspaceCreationAlertTitle=Oprettelse af arbejdsområde +developerForceSshTty=Fremtving SSH TTY +developerForceSshTtyDescription=Få alle SSH-forbindelser til at tildele en pty for at teste understøttelsen af en manglende stderr og en pty. +ttyWarning=Forbindelsen har tvangstildelt en pty/tty og giver ikke en separat stderr-strøm.\n\nDet kan føre til et par problemer.\n\nHvis du kan, så prøv at få forbindelseskommandoen til ikke at tildele en pty. diff --git a/lang/app/strings/translations_de.properties b/lang/app/strings/translations_de.properties index bec06bc85..3567c2c5a 100644 --- a/lang/app/strings/translations_de.properties +++ b/lang/app/strings/translations_de.properties @@ -494,3 +494,6 @@ workspaceNameDescription=Der Anzeigename des Arbeitsbereichs workspacePath=Pfad zum Arbeitsbereich workspacePathDescription=Der Ort des Datenverzeichnisses des Arbeitsbereichs workspaceCreationAlertTitle=Arbeitsbereich erstellen +developerForceSshTty=SSH TTY erzwingen +developerForceSshTtyDescription=Lass alle SSH-Verbindungen ein pty zuweisen, um die Unterstützung für einen fehlenden stderr und ein pty zu testen. +ttyWarning=Die Verbindung hat zwangsweise ein pty/tty zugewiesen und stellt keinen separaten stderr-Stream zur Verfügung.\n\nDas kann zu einigen Problemen führen.\n\nWenn du kannst, solltest du dafür sorgen, dass der Verbindungsbefehl kein pty zuweist. diff --git a/lang/app/strings/translations_en.properties b/lang/app/strings/translations_en.properties index 0d3a8059d..3b484118e 100644 --- a/lang/app/strings/translations_en.properties +++ b/lang/app/strings/translations_en.properties @@ -498,3 +498,6 @@ workspaceNameDescription=The display name of the workspace workspacePath=Workspace path workspacePathDescription=The location of the workspace data directory workspaceCreationAlertTitle=Workspace creation +developerForceSshTty=Force SSH TTY +developerForceSshTtyDescription=Make all SSH connections allocate a pty to test the support for a missing stderr and a pty. +ttyWarning=The connection has forcefully allocated a pty/tty and does not provide a separate stderr stream.\n\nThis might lead to a few problems.\n\nIf you can, look into making the connection command not allocate a pty. diff --git a/lang/app/strings/translations_es.properties b/lang/app/strings/translations_es.properties index d95a5fcb0..dfbe4f77b 100644 --- a/lang/app/strings/translations_es.properties +++ b/lang/app/strings/translations_es.properties @@ -481,3 +481,6 @@ workspaceNameDescription=El nombre para mostrar del espacio de trabajo workspacePath=Ruta del espacio de trabajo workspacePathDescription=La ubicación del directorio de datos del espacio de trabajo workspaceCreationAlertTitle=Creación de espacios de trabajo +developerForceSshTty=Forzar SSH TTY +developerForceSshTtyDescription=Haz que todas las conexiones SSH asignen una pty para probar la compatibilidad con una stderr y una pty ausentes. +ttyWarning=La conexión ha asignado forzosamente un pty/tty y no proporciona un flujo stderr separado.\n\nEsto puede provocar algunos problemas.\n\nSi puedes, intenta que el comando de conexión no asigne una pty. diff --git a/lang/app/strings/translations_fr.properties b/lang/app/strings/translations_fr.properties index 4fbf5b9dd..a66860997 100644 --- a/lang/app/strings/translations_fr.properties +++ b/lang/app/strings/translations_fr.properties @@ -481,3 +481,6 @@ workspaceNameDescription=Le nom d'affichage de l'espace de travail workspacePath=Chemin d'accès à l'espace de travail workspacePathDescription=L'emplacement du répertoire de données de l'espace de travail workspaceCreationAlertTitle=Création d'un espace de travail +developerForceSshTty=Force SSH TTY +developerForceSshTtyDescription=Fais en sorte que toutes les connexions SSH allouent un pty pour tester la prise en charge d'un stderr et d'un pty manquants. +ttyWarning=La connexion a alloué de force un pty/tty et ne fournit pas de flux stderr séparé.\n\nCela peut entraîner quelques problèmes.\n\nSi tu le peux, essaie de faire en sorte que la commande de connexion n'alloue pas de pty. diff --git a/lang/app/strings/translations_it.properties b/lang/app/strings/translations_it.properties index 39c429568..aee2efbd2 100644 --- a/lang/app/strings/translations_it.properties +++ b/lang/app/strings/translations_it.properties @@ -481,3 +481,6 @@ workspaceNameDescription=Il nome di visualizzazione dell'area di lavoro workspacePath=Percorso dello spazio di lavoro workspacePathDescription=La posizione della directory dei dati dell'area di lavoro workspaceCreationAlertTitle=Creazione di uno spazio di lavoro +developerForceSshTty=Forza SSH TTY +developerForceSshTtyDescription=Fai in modo che tutte le connessioni SSH allocino una pty per testare il supporto di una stderr e di una pty mancanti. +ttyWarning=La connessione ha allocato forzatamente una pty/tty e non fornisce un flusso stderr separato.\n\nQuesto potrebbe causare alcuni problemi.\n\nSe puoi, cerca di fare in modo che il comando di connessione non allarghi una pty. diff --git a/lang/app/strings/translations_ja.properties b/lang/app/strings/translations_ja.properties index c8f13dc6f..f71ef71b9 100644 --- a/lang/app/strings/translations_ja.properties +++ b/lang/app/strings/translations_ja.properties @@ -481,3 +481,6 @@ workspaceNameDescription=ワークスペースの表示名 workspacePath=ワークスペースのパス workspacePathDescription=ワークスペースのデータディレクトリの場所 workspaceCreationAlertTitle=ワークスペースの作成 +developerForceSshTty=強制SSH TTY +developerForceSshTtyDescription=すべてのSSHコネクションにptyを割り当て、stderrとptyがない場合のサポートをテストする。 +ttyWarning=接続が強制的にpty/ttyを割り当て、個別のstderrストリームを提供しない。\n\nこれはいくつかの問題を引き起こす可能性がある。\n\n可能であれば、接続コマンドで pty を割り当てないようにすることを検討してほしい。 diff --git a/lang/app/strings/translations_nl.properties b/lang/app/strings/translations_nl.properties index e651cb8fb..de00729dd 100644 --- a/lang/app/strings/translations_nl.properties +++ b/lang/app/strings/translations_nl.properties @@ -481,3 +481,6 @@ workspaceNameDescription=De weergavenaam van de werkruimte workspacePath=Werkruimte pad workspacePathDescription=De locatie van de gegevensmap van de werkruimte workspaceCreationAlertTitle=Werkruimte maken +developerForceSshTty=SSH TTY afdwingen +developerForceSshTtyDescription=Laat alle SSH-verbindingen een pty toewijzen om de ondersteuning voor een ontbrekende stderr en een pty te testen. +ttyWarning=De verbinding heeft geforceerd een pty/tty toegewezen en biedt geen aparte stderr stream.\n\nDit kan tot een paar problemen leiden.\n\nAls je kunt, kijk dan of je het connection commando geen pty kunt laten toewijzen. diff --git a/lang/app/strings/translations_pt.properties b/lang/app/strings/translations_pt.properties index 7fba4e1cb..83adc3378 100644 --- a/lang/app/strings/translations_pt.properties +++ b/lang/app/strings/translations_pt.properties @@ -481,3 +481,6 @@ workspaceNameDescription=O nome de apresentação do espaço de trabalho workspacePath=Caminho do espaço de trabalho workspacePathDescription=A localização do diretório de dados do espaço de trabalho workspaceCreationAlertTitle=Criação de espaço de trabalho +developerForceSshTty=Força o SSH TTY +developerForceSshTtyDescription=Faz com que todas as ligações SSH atribuam um pty para testar o suporte para um stderr e um pty em falta. +ttyWarning=A ligação atribuiu à força um pty/tty e não fornece um fluxo stderr separado.\n\nIsto pode levar a alguns problemas.\n\nSe puderes, tenta fazer com que o comando de ligação não atribua um pty. diff --git a/lang/app/strings/translations_ru.properties b/lang/app/strings/translations_ru.properties index a8cb72d1f..a4efffb55 100644 --- a/lang/app/strings/translations_ru.properties +++ b/lang/app/strings/translations_ru.properties @@ -481,3 +481,6 @@ workspaceNameDescription=Отображаемое имя рабочей обла workspacePath=Путь к рабочему пространству workspacePathDescription=Расположение каталога данных рабочей области workspaceCreationAlertTitle=Создание рабочего пространства +developerForceSshTty=Принудительный SSH TTY +developerForceSshTtyDescription=Заставь все SSH-соединения выделять pty, чтобы проверить поддержку отсутствующего stderr и pty. +ttyWarning=Соединение принудительно выделило pty/tty и не предоставляет отдельный поток stderr.\n\nЭто может привести к нескольким проблемам.\n\nЕсли можешь, попробуй сделать так, чтобы команда подключения не выделяла pty. diff --git a/lang/app/strings/translations_tr.properties b/lang/app/strings/translations_tr.properties index 8b970e969..4e4932b77 100644 --- a/lang/app/strings/translations_tr.properties +++ b/lang/app/strings/translations_tr.properties @@ -482,3 +482,6 @@ workspaceNameDescription=Çalışma alanının görünen adı workspacePath=Çalışma alanı yolu workspacePathDescription=Çalışma alanı veri dizininin konumu workspaceCreationAlertTitle=Çalışma alanı oluşturma +developerForceSshTty=SSH TTY'yi Zorla +developerForceSshTtyDescription=Eksik bir stderr ve bir pty desteğini test etmek için tüm SSH bağlantılarının bir pty ayırmasını sağlayın. +ttyWarning=Bağlantı zorla bir pty/tty ayırmış ve ayrı bir stderr akışı sağlamıyor.\n\nBu durum birkaç soruna yol açabilir.\n\nEğer yapabiliyorsanız, bağlantı komutunun bir pty tahsis etmemesini sağlayın. diff --git a/lang/app/strings/translations_zh.properties b/lang/app/strings/translations_zh.properties index d9860737b..9f3d78b31 100644 --- a/lang/app/strings/translations_zh.properties +++ b/lang/app/strings/translations_zh.properties @@ -481,3 +481,6 @@ workspaceNameDescription=工作区的显示名称 workspacePath=工作区路径 workspacePathDescription=工作区数据目录的位置 workspaceCreationAlertTitle=创建工作区 +developerForceSshTty=强制 SSH TTY +developerForceSshTtyDescription=让所有 SSH 连接都分配一个 pty,以测试对缺失的 stderr 和 pty 的支持。 +ttyWarning=连接强行分配了 pty/tty,且未提供单独的 stderr 流。\n\n这可能会导致一些问题。\n\n如果可以,请考虑让连接命令不分配 pty。