From da42eb578fa080a99c8bf2ef6fab5ad178440493 Mon Sep 17 00:00:00 2001 From: crschnick Date: Tue, 13 Aug 2024 14:48:56 +0000 Subject: [PATCH] More terminal launcher rework --- app/build.gradle | 4 +- .../beacon/impl/SshLaunchExchangeImpl.java | 2 +- .../app/prefs/ExternalRdpClientType.java | 66 +++++- .../io/xpipe/app/storage/DataStorage.java | 3 + .../app/terminal/ExternalTerminalType.java | 211 +++++++++++++++++- .../io/xpipe/app/util/SshLocalBridge.java | 21 +- .../app/util/TerminalLauncherManager.java | 5 +- dist/licenses/slf4j.properties | 2 +- lang/app/strings/fixed_en.properties | 5 + lang/app/strings/translations_da.properties | 2 + lang/app/strings/translations_de.properties | 2 + lang/app/strings/translations_en.properties | 2 + lang/app/strings/translations_es.properties | 2 + lang/app/strings/translations_fr.properties | 2 + lang/app/strings/translations_it.properties | 2 + lang/app/strings/translations_ja.properties | 2 + lang/app/strings/translations_nl.properties | 2 + lang/app/strings/translations_pt.properties | 2 + lang/app/strings/translations_ru.properties | 2 + lang/app/strings/translations_tr.properties | 2 + lang/app/strings/translations_zh.properties | 2 + lang/app/texts/termiusSetup_da.md | 5 + lang/app/texts/termiusSetup_de.md | 5 + lang/app/texts/termiusSetup_en.md | 5 + lang/app/texts/termiusSetup_es.md | 5 + lang/app/texts/termiusSetup_fr.md | 5 + lang/app/texts/termiusSetup_it.md | 5 + lang/app/texts/termiusSetup_ja.md | 5 + lang/app/texts/termiusSetup_nl.md | 5 + lang/app/texts/termiusSetup_pt.md | 5 + lang/app/texts/termiusSetup_ru.md | 5 + lang/app/texts/termiusSetup_tr.md | 5 + lang/app/texts/termiusSetup_zh.md | 5 + lang/app/texts/xshellSetup_da.md | 5 + lang/app/texts/xshellSetup_de.md | 5 + lang/app/texts/xshellSetup_en.md | 5 + lang/app/texts/xshellSetup_es.md | 5 + lang/app/texts/xshellSetup_fr.md | 5 + lang/app/texts/xshellSetup_it.md | 5 + lang/app/texts/xshellSetup_ja.md | 5 + lang/app/texts/xshellSetup_nl.md | 5 + lang/app/texts/xshellSetup_pt.md | 5 + lang/app/texts/xshellSetup_ru.md | 5 + lang/app/texts/xshellSetup_tr.md | 5 + lang/app/texts/xshellSetup_zh.md | 5 + 45 files changed, 443 insertions(+), 20 deletions(-) create mode 100644 lang/app/texts/termiusSetup_da.md create mode 100644 lang/app/texts/termiusSetup_de.md create mode 100644 lang/app/texts/termiusSetup_en.md create mode 100644 lang/app/texts/termiusSetup_es.md create mode 100644 lang/app/texts/termiusSetup_fr.md create mode 100644 lang/app/texts/termiusSetup_it.md create mode 100644 lang/app/texts/termiusSetup_ja.md create mode 100644 lang/app/texts/termiusSetup_nl.md create mode 100644 lang/app/texts/termiusSetup_pt.md create mode 100644 lang/app/texts/termiusSetup_ru.md create mode 100644 lang/app/texts/termiusSetup_tr.md create mode 100644 lang/app/texts/termiusSetup_zh.md create mode 100644 lang/app/texts/xshellSetup_da.md create mode 100644 lang/app/texts/xshellSetup_de.md create mode 100644 lang/app/texts/xshellSetup_en.md create mode 100644 lang/app/texts/xshellSetup_es.md create mode 100644 lang/app/texts/xshellSetup_fr.md create mode 100644 lang/app/texts/xshellSetup_it.md create mode 100644 lang/app/texts/xshellSetup_ja.md create mode 100644 lang/app/texts/xshellSetup_nl.md create mode 100644 lang/app/texts/xshellSetup_pt.md create mode 100644 lang/app/texts/xshellSetup_ru.md create mode 100644 lang/app/texts/xshellSetup_tr.md create mode 100644 lang/app/texts/xshellSetup_zh.md diff --git a/app/build.gradle b/app/build.gradle index a3cf4975..ced480ca 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -65,8 +65,8 @@ dependencies { api group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0" api group: 'org.kordamp.ikonli', name: 'ikonli-material-pack', version: "12.2.0" api group: 'org.kordamp.ikonli', name: 'ikonli-feather-pack', version: "12.2.0" - api group: 'org.slf4j', name: 'slf4j-api', version: '2.0.15' - api group: 'org.slf4j', name: 'slf4j-jdk-platform-logging', version: '2.0.15' + api group: 'org.slf4j', name: 'slf4j-api', version: '2.0.16' + api group: 'org.slf4j', name: 'slf4j-jdk-platform-logging', version: '2.0.16' api 'io.xpipe:modulefs:0.1.5' api 'net.synedra:validatorfx:0.4.2' api files("$rootDir/gradle/gradle_scripts/atlantafx-base-2.0.2.jar") diff --git a/app/src/main/java/io/xpipe/app/beacon/impl/SshLaunchExchangeImpl.java b/app/src/main/java/io/xpipe/app/beacon/impl/SshLaunchExchangeImpl.java index 4a548c06..951a8367 100644 --- a/app/src/main/java/io/xpipe/app/beacon/impl/SshLaunchExchangeImpl.java +++ b/app/src/main/java/io/xpipe/app/beacon/impl/SshLaunchExchangeImpl.java @@ -25,7 +25,7 @@ public class SshLaunchExchangeImpl extends SshLaunchExchange { } TerminalLauncherManager.submitAsync(UUID.randomUUID(), ((ShellStore) DataStorage.get().local().getStore()).control(), TerminalInitScriptConfig.ofName("abc"),null); - var r = TerminalLauncherManager.waitForFirstLaunch(); + var r = TerminalLauncherManager.waitForNextLaunch(); var c = ProcessControlProvider.get().getEffectiveLocalDialect().getOpenScriptCommand(r.toString()).buildBaseParts(null); return Response.builder().command(c).build(); } diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalRdpClientType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalRdpClientType.java index 4e88b423..c3789d9a 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalRdpClientType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalRdpClientType.java @@ -6,9 +6,9 @@ import io.xpipe.app.util.*; import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.OsType; import io.xpipe.core.util.SecretValue; - import lombok.Value; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; @@ -16,6 +16,14 @@ import java.util.function.Supplier; public interface ExternalRdpClientType extends PrefsChoiceValue { + public static ExternalRdpClientType getApplicationLauncher() { + if (OsType.getLocal() == OsType.WINDOWS) { + return MSTSC; + } else { + return AppPrefs.get().rdpClientType().getValue(); + } + } + ExternalRdpClientType MSTSC = new PathCheckType("app.mstsc", "mstsc.exe", false) { @Override @@ -62,6 +70,37 @@ public interface ExternalRdpClientType extends PrefsChoiceValue { return cmd.readStdoutOrThrow(); } }; + + ExternalRdpClientType DEVOLUTIONS = new WindowsType("app.devolutions", "RemoteDesktopManager") { + + @Override + protected Optional determineInstallation() { + try { + var r = WindowsRegistry.local().readValue(WindowsRegistry.HKEY_LOCAL_MACHINE, "SOFTWARE\\Classes\\rdm\\DefaultIcon"); + return r.map(Path::of); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).omit().handle(); + return Optional.empty(); + } + } + + @Override + protected void execute(Path file, LaunchConfiguration configuration) throws Exception { + var config = writeConfig(configuration.getConfig()); + LocalShell.getShell().executeSimpleCommand(CommandBuilder.of().addFile(file.toString()).addFile(config.toString()).discardOutput()); + ThreadHelper.runFailableAsync(() -> { + // Startup is slow + ThreadHelper.sleep(10000); + Files.delete(config); + }); + } + + @Override + public boolean supportsPasswordPassing() { + return false; + } + }; + ExternalRdpClientType REMMINA = new PathCheckType("app.remmina", "remmina", true) { @Override @@ -96,7 +135,7 @@ public interface ExternalRdpClientType extends PrefsChoiceValue { } }; ExternalRdpClientType CUSTOM = new CustomType(); - List WINDOWS_CLIENTS = List.of(MSTSC); + List WINDOWS_CLIENTS = List.of(MSTSC, DEVOLUTIONS); List LINUX_CLIENTS = List.of(REMMINA); List MACOS_CLIENTS = List.of(MICROSOFT_REMOTE_DESKTOP_MACOS_APP); @@ -145,6 +184,29 @@ public interface ExternalRdpClientType extends PrefsChoiceValue { SecretValue password; } + abstract class WindowsType extends ExternalApplicationType.WindowsType implements ExternalRdpClientType { + + public WindowsType(String id, String executable) { + super(id, executable); + } + + @Override + public void launch(LaunchConfiguration configuration) throws Exception { + var location = determineFromPath(); + if (location.isEmpty()) { + location = determineInstallation(); + if (location.isEmpty()) { + throw new IOException("Unable to find installation of " + + toTranslatedString().getValue()); + } + } + + execute(location.get(), configuration); + } + + protected abstract void execute(Path file, LaunchConfiguration configuration) throws Exception; + } + abstract class PathCheckType extends ExternalApplicationType.PathApplication implements ExternalRdpClientType { public PathCheckType(String id, String executable, boolean explicityAsync) { diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorage.java b/app/src/main/java/io/xpipe/app/storage/DataStorage.java index 4cc3d110..afa5119c 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java @@ -2,6 +2,7 @@ package io.xpipe.app.storage; import io.xpipe.app.comp.store.StoreSortMode; import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.FixedHierarchyStore; import io.xpipe.app.util.ThreadHelper; @@ -120,9 +121,11 @@ public abstract class DataStorage { } public void forceRewrite() { + TrackEvent.info("Starting forced storage rewrite"); getStoreEntries().forEach(dataStoreEntry -> { dataStoreEntry.reassignStore(); }); + TrackEvent.info("Finished forced storage rewrite"); } private void dispose() { diff --git a/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java index 7cb26561..13e82848 100644 --- a/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java @@ -1,5 +1,9 @@ package io.xpipe.app.terminal; +import io.xpipe.app.comp.base.MarkdownComp; +import io.xpipe.app.core.AppCache; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.core.window.AppWindowHelper; import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.ExternalApplicationType; @@ -8,18 +12,181 @@ import io.xpipe.app.util.*; import io.xpipe.core.process.*; import io.xpipe.core.store.FilePath; import io.xpipe.core.util.FailableFunction; - +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; import lombok.Getter; import lombok.Value; import lombok.With; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; import java.util.*; public interface ExternalTerminalType extends PrefsChoiceValue { +// ExternalTerminalType PUTTY = new WindowsType("app.putty","putty") { +// +// @Override +// protected Optional determineInstallation() { +// try { +// var r = WindowsRegistry.local().readValue(WindowsRegistry.HKEY_LOCAL_MACHINE, +// "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\Xshell.exe"); +// return r.map(Path::of); +// } catch (Exception e) { +// ErrorEvent.fromThrowable(e).omit().handle(); +// return Optional.empty(); +// } +// } +// +// @Override +// public boolean supportsTabs() { +// return true; +// } +// +// @Override +// public boolean isRecommended() { +// return false; +// } +// +// @Override +// public boolean supportsColoredTitle() { +// return false; +// } +// +// @Override +// protected void execute(Path file, LaunchConfiguration configuration) throws Exception { +// try (var sc = LocalShell.getShell()) { +// SshLocalBridge.init(); +// var b = SshLocalBridge.get(); +// var command = CommandBuilder.of().addFile(file.toString()).add("-ssh", "localhost", "-l").addQuoted(b.getUser()) +// .add("-i").addFile(b.getIdentityKey().toString()).add("-P", "" + b.getPort()).add("-hostkey").addFile(b.getPubHostKey().toString()); +// sc.executeSimpleCommand(command); +// } +// } +// }; + + ExternalTerminalType XSHELL = new WindowsType("app.xShell","Xshell") { + + @Override + protected Optional determineInstallation() { + try { + var r = WindowsRegistry.local().readValue(WindowsRegistry.HKEY_LOCAL_MACHINE, + "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\Xshell.exe"); + return r.map(Path::of); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).omit().handle(); + return Optional.empty(); + } + } + + @Override + public boolean supportsTabs() { + return true; + } + + @Override + public boolean isRecommended() { + return false; + } + + @Override + public boolean supportsColoredTitle() { + return false; + } + + @Override + protected void execute(Path file, LaunchConfiguration configuration) throws Exception { + SshLocalBridge.init(); + if (!showInfo()) { + return; + } + + try (var sc = LocalShell.getShell()) { + var b = SshLocalBridge.get(); + var command = CommandBuilder.of().addFile(file.toString()).add("-url").addQuoted("ssh://" + b.getUser() + "@localhost:" + b.getPort()) + .add("-i", "xpipe_bridge"); + sc.executeSimpleCommand(command); + } + } + + private boolean showInfo() { + boolean set = AppCache.get("xshellSetup", Boolean.class, () -> false); + if (set) { + return true; + } + + var b = SshLocalBridge.get(); + var r = AppWindowHelper.showBlockingAlert( + alert -> { + alert.setTitle(AppI18n.get("xshellSetup")); + alert.setAlertType(Alert.AlertType.NONE); + + var activated = AppI18n.get().getMarkdownDocumentation("app:xshellSetup"); + var markdown = new MarkdownComp(activated, s -> s.formatted(b.getIdentityKey(), "xpipe_bridge")) + .prefWidth(450) + .prefHeight(400) + .createRegion(); + alert.getDialogPane().setContent(markdown); + + alert.getButtonTypes().add(new ButtonType(AppI18n.get("ok"), ButtonBar.ButtonData.OK_DONE)); + }); + r.filter(buttonType -> buttonType.getButtonData().isDefaultButton()); + r.ifPresent(buttonType -> { + AppCache.update("xshellSetup", true); + }); + return r.isPresent(); + } + }; + + ExternalTerminalType SECURECRT = new WindowsType("app.secureCrt","SecureCRT") { + + @Override + protected Optional determineInstallation() { + try (var sc = LocalShell.getShell().start()) { + var env = sc.executeSimpleStringCommand( + sc.getShellDialect().getPrintEnvironmentVariableCommand("ProgramFiles")); + var file = Path.of(env, "VanDyke Software\\SecureCRT\\SecureCRT.exe"); + if (!Files.exists(file)) { + return Optional.empty(); + } + + return Optional.of(file); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).omit().handle(); + return Optional.empty(); + } + } + + @Override + public boolean supportsTabs() { + return true; + } + + @Override + public boolean isRecommended() { + return false; + } + + @Override + public boolean supportsColoredTitle() { + return false; + } + + @Override + protected void execute(Path file, LaunchConfiguration configuration) throws Exception { + try (var sc = LocalShell.getShell()) { + SshLocalBridge.init(); + var b = SshLocalBridge.get(); + var command = CommandBuilder.of().addFile(file.toString()).add("/T").add("/SSH2", "/ACCEPTHOSTKEYS", "/I").addFile( + b.getIdentityKey().toString()).add("/P", "" + b.getPort()).add("/L").addQuoted(b.getUser()).add("localhost"); + sc.executeSimpleCommand(command); + } + } + }; + ExternalTerminalType MOBAXTERM = new WindowsType("app.mobaXterm","MobaXterm") { @Override @@ -108,12 +275,44 @@ public interface ExternalTerminalType extends PrefsChoiceValue { @Override public void launch(LaunchConfiguration configuration) throws Exception { SshLocalBridge.init(); + if (!showInfo()) { + return; + } + var name = "xpipe_bridge"; var host = "localhost"; - var port = 21722; - var user = System.getProperty("user.name"); + var port = SshLocalBridge.get().getPort(); + var user = SshLocalBridge.get().getUser(); Hyperlinks.open("termius://app/host-sharing#label=" + name + "&ip=" + host + "&port=" + port + "&username=" - + user + "&os=windows"); + + user + "&os=undefined"); + } + + private boolean showInfo() { + boolean set = AppCache.get("termiusSetup", Boolean.class, () -> false); + if (set) { + return true; + } + + var b = SshLocalBridge.get(); + var r = AppWindowHelper.showBlockingAlert( + alert -> { + alert.setTitle(AppI18n.get("termiusSetup")); + alert.setAlertType(Alert.AlertType.NONE); + + var activated = AppI18n.get().getMarkdownDocumentation("app:termiusSetup"); + var markdown = new MarkdownComp(activated, s -> s.formatted(b.getIdentityKey(), "xpipe_bridge")) + .prefWidth(450) + .prefHeight(400) + .createRegion(); + alert.getDialogPane().setContent(markdown); + + alert.getButtonTypes().add(new ButtonType(AppI18n.get("ok"), ButtonBar.ButtonData.OK_DONE)); + }); + r.filter(buttonType -> buttonType.getButtonData().isDefaultButton()); + r.ifPresent(buttonType -> { + AppCache.update("termiusSetup", true); + }); + return r.isPresent(); } }; @@ -747,8 +946,10 @@ public interface ExternalTerminalType extends PrefsChoiceValue { TabbyTerminalType.TABBY_WINDOWS, AlacrittyTerminalType.ALACRITTY_WINDOWS, WezTerminalType.WEZTERM_WINDOWS, - TERMIUS, MOBAXTERM, + SECURECRT, + TERMIUS, + XSHELL, CMD, PWSH, POWERSHELL); diff --git a/app/src/main/java/io/xpipe/app/util/SshLocalBridge.java b/app/src/main/java/io/xpipe/app/util/SshLocalBridge.java index bec294c7..0afe38e7 100644 --- a/app/src/main/java/io/xpipe/app/util/SshLocalBridge.java +++ b/app/src/main/java/io/xpipe/app/util/SshLocalBridge.java @@ -20,6 +20,10 @@ public class SshLocalBridge { private static SshLocalBridge INSTANCE; + public static SshLocalBridge get() { + return INSTANCE; + } + private final Path directory; private final int port; private final String user; @@ -33,19 +37,19 @@ public class SshLocalBridge { } public Path getPubHostKey() { - return directory.resolve("host_key.pub"); + return directory.resolve("xpipe_bridge_host_key.pub"); } public Path getHostKey() { - return directory.resolve("host_key"); + return directory.resolve("xpipe_bridge_host_key"); } public Path getPubIdentityKey() { - return directory.resolve("identity.pub"); + return directory.resolve("xpipe_bridge.pub"); } public Path getIdentityKey() { - return directory.resolve("identity"); + return directory.resolve("xpipe_bridge"); } public Path getConfig() { @@ -66,12 +70,15 @@ public class SshLocalBridge { var hostKey = INSTANCE.getHostKey(); if (!sc.getShellDialect().createFileExistsCommand(sc, hostKey.toString()).executeAndCheck()) { - sc.executeSimpleCommand("ssh-keygen -q -N \"\" -t ed25519 -f \"" + hostKey + "\""); + sc.command(CommandBuilder.of().add("ssh-keygen", "-q", "-N") + .addQuoted("").add("-C").addQuoted("XPipe SSH bridge host key") + .add("-t", "ed25519", "-f").addFile(hostKey.toString())).execute(); } var idKey = INSTANCE.getIdentityKey(); if (!sc.getShellDialect().createFileExistsCommand(sc, idKey.toString()).executeAndCheck()) { - sc.executeSimpleCommand("ssh-keygen -q -N \"\" -t ed25519 -f \"" + idKey + "\""); + sc.command(CommandBuilder.of().add("ssh-keygen", "-q", "-N") + .addQuoted("").add("-C").addQuoted("XPipe SSH bridge identity").add("-t", "ed25519", "-f").addFile(idKey.toString())).execute(); } var config = INSTANCE.getConfig(); @@ -92,7 +99,7 @@ public class SshLocalBridge { .formatted(command, pidFile.toString(), "" + port, INSTANCE.getHostKey().toString(), INSTANCE.getPubIdentityKey());; Files.writeString(config, content); - INSTANCE.updateConfig(); + // INSTANCE.updateConfig(); var exec = getSshd(sc); var launchCommand = CommandBuilder.of().addFile(exec).add("-f").addFile(INSTANCE.getConfig().toString()).add("-p", "" + port); diff --git a/app/src/main/java/io/xpipe/app/util/TerminalLauncherManager.java b/app/src/main/java/io/xpipe/app/util/TerminalLauncherManager.java index 4bec639d..6b97ca3e 100644 --- a/app/src/main/java/io/xpipe/app/util/TerminalLauncherManager.java +++ b/app/src/main/java/io/xpipe/app/util/TerminalLauncherManager.java @@ -72,12 +72,13 @@ public class TerminalLauncherManager { return latch; } - public static Path waitForFirstLaunch() throws BeaconClientException, BeaconServerException { + public static Path waitForNextLaunch() throws BeaconClientException, BeaconServerException { if (entries.isEmpty()) { throw new BeaconClientException("Unknown launch request"); } var first = entries.firstEntry(); + entries.remove(first.getKey()); return waitForCompletion(first.getKey()); } @@ -94,8 +95,8 @@ public class TerminalLauncherManager { } var r = e.getResult(); - entries.remove(request); if (r instanceof ResultFailure failure) { + entries.remove(request); var t = failure.getThrowable(); throw new BeaconServerException(t); } diff --git a/dist/licenses/slf4j.properties b/dist/licenses/slf4j.properties index 2dc87d2c..d71ea909 100644 --- a/dist/licenses/slf4j.properties +++ b/dist/licenses/slf4j.properties @@ -1,4 +1,4 @@ name=SLF4J -version=2.0.15 +version=2.0.16 license=MIT License link=https://www.slf4j.org/ \ No newline at end of file diff --git a/lang/app/strings/fixed_en.properties b/lang/app/strings/fixed_en.properties index 5ec85025..2b04a968 100644 --- a/lang/app/strings/fixed_en.properties +++ b/lang/app/strings/fixed_en.properties @@ -61,3 +61,8 @@ dashlane=Dashlane lastpass=LastPass macosKeychain=macOS keychain windowsTerminalCanary=Windows Terminal Canary +secureCrt=SecureCRT +xShell=Xshell +mobaXterm=MobaXterm +termius=Termius +devolutions=Devolutions diff --git a/lang/app/strings/translations_da.properties b/lang/app/strings/translations_da.properties index e482a3e7..6fea7fda 100644 --- a/lang/app/strings/translations_da.properties +++ b/lang/app/strings/translations_da.properties @@ -503,3 +503,5 @@ 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. +xshellSetup=Xshell-opsætning +termiusSetup=Termius-opsætning diff --git a/lang/app/strings/translations_de.properties b/lang/app/strings/translations_de.properties index 8b238fb5..43d00ca5 100644 --- a/lang/app/strings/translations_de.properties +++ b/lang/app/strings/translations_de.properties @@ -497,3 +497,5 @@ 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. +xshellSetup=Xshell-Einrichtung +termiusSetup=Termius Einrichtung diff --git a/lang/app/strings/translations_en.properties b/lang/app/strings/translations_en.properties index 75bf8f7f..9e748713 100644 --- a/lang/app/strings/translations_en.properties +++ b/lang/app/strings/translations_en.properties @@ -501,3 +501,5 @@ 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. +xshellSetup=Xshell setup +termiusSetup=Termius setup diff --git a/lang/app/strings/translations_es.properties b/lang/app/strings/translations_es.properties index 0f6f65c2..ce354fd6 100644 --- a/lang/app/strings/translations_es.properties +++ b/lang/app/strings/translations_es.properties @@ -484,3 +484,5 @@ 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. +xshellSetup=Configuración de Xshell +termiusSetup=Configuración de Termius diff --git a/lang/app/strings/translations_fr.properties b/lang/app/strings/translations_fr.properties index 00696cef..eb0d8877 100644 --- a/lang/app/strings/translations_fr.properties +++ b/lang/app/strings/translations_fr.properties @@ -484,3 +484,5 @@ 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. +xshellSetup=Configuration de Xshell +termiusSetup=Installation de Termius diff --git a/lang/app/strings/translations_it.properties b/lang/app/strings/translations_it.properties index b6857458..ed713c4e 100644 --- a/lang/app/strings/translations_it.properties +++ b/lang/app/strings/translations_it.properties @@ -484,3 +484,5 @@ 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. +xshellSetup=Configurazione di Xshell +termiusSetup=Configurazione di Termius diff --git a/lang/app/strings/translations_ja.properties b/lang/app/strings/translations_ja.properties index 23a32f17..8c57615f 100644 --- a/lang/app/strings/translations_ja.properties +++ b/lang/app/strings/translations_ja.properties @@ -484,3 +484,5 @@ workspaceCreationAlertTitle=ワークスペースの作成 developerForceSshTty=強制SSH TTY developerForceSshTtyDescription=すべてのSSHコネクションにptyを割り当て、stderrとptyがない場合のサポートをテストする。 ttyWarning=接続が強制的にpty/ttyを割り当て、個別のstderrストリームを提供しない。\n\nこれはいくつかの問題を引き起こす可能性がある。\n\n可能であれば、接続コマンドで pty を割り当てないようにすることを検討してほしい。 +xshellSetup=Xshellのセットアップ +termiusSetup=テルミウスのセットアップ diff --git a/lang/app/strings/translations_nl.properties b/lang/app/strings/translations_nl.properties index 956873e8..78917b34 100644 --- a/lang/app/strings/translations_nl.properties +++ b/lang/app/strings/translations_nl.properties @@ -484,3 +484,5 @@ 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. +xshellSetup=Xshell installatie +termiusSetup=Termius installatie diff --git a/lang/app/strings/translations_pt.properties b/lang/app/strings/translations_pt.properties index 911b410b..7b767555 100644 --- a/lang/app/strings/translations_pt.properties +++ b/lang/app/strings/translations_pt.properties @@ -484,3 +484,5 @@ 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. +xshellSetup=Configuração do Xshell +termiusSetup=Configuração do Termius diff --git a/lang/app/strings/translations_ru.properties b/lang/app/strings/translations_ru.properties index 762edf59..855ee656 100644 --- a/lang/app/strings/translations_ru.properties +++ b/lang/app/strings/translations_ru.properties @@ -484,3 +484,5 @@ workspaceCreationAlertTitle=Создание рабочего пространс developerForceSshTty=Принудительный SSH TTY developerForceSshTtyDescription=Заставь все SSH-соединения выделять pty, чтобы проверить поддержку отсутствующего stderr и pty. ttyWarning=Соединение принудительно выделило pty/tty и не предоставляет отдельный поток stderr.\n\nЭто может привести к нескольким проблемам.\n\nЕсли можешь, попробуй сделать так, чтобы команда подключения не выделяла pty. +xshellSetup=Настройка Xshell +termiusSetup=Настройка Термиуса diff --git a/lang/app/strings/translations_tr.properties b/lang/app/strings/translations_tr.properties index b4ff5482..35ce7275 100644 --- a/lang/app/strings/translations_tr.properties +++ b/lang/app/strings/translations_tr.properties @@ -485,3 +485,5 @@ 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. +xshellSetup=Xshell kurulumu +termiusSetup=Termius kurulumu diff --git a/lang/app/strings/translations_zh.properties b/lang/app/strings/translations_zh.properties index ae866d43..1c7142e5 100644 --- a/lang/app/strings/translations_zh.properties +++ b/lang/app/strings/translations_zh.properties @@ -484,3 +484,5 @@ workspaceCreationAlertTitle=创建工作区 developerForceSshTty=强制 SSH TTY developerForceSshTtyDescription=让所有 SSH 连接都分配一个 pty,以测试对缺失的 stderr 和 pty 的支持。 ttyWarning=连接强行分配了 pty/tty,且未提供单独的 stderr 流。\n\n这可能会导致一些问题。\n\n如果可以,请考虑让连接命令不分配 pty。 +xshellSetup=Xshell 设置 +termiusSetup=Termius 设置 diff --git a/lang/app/texts/termiusSetup_da.md b/lang/app/texts/termiusSetup_da.md new file mode 100644 index 00000000..5fe5031a --- /dev/null +++ b/lang/app/texts/termiusSetup_da.md @@ -0,0 +1,5 @@ +# Termius setup + +For at bruge Termius som din terminal kan du forbinde den til XPipe SSH-broen. Det kan ske automatisk, når den lokale ssh-nøgle til broen er blevet tilføjet til Termius. + +Det eneste, du skal gøre manuelt, er at tilføje den private nøglefil `%s` til Termius først. \ No newline at end of file diff --git a/lang/app/texts/termiusSetup_de.md b/lang/app/texts/termiusSetup_de.md new file mode 100644 index 00000000..27305fb0 --- /dev/null +++ b/lang/app/texts/termiusSetup_de.md @@ -0,0 +1,5 @@ +# Termius setup + +Um Termius als Terminal zu verwenden, kannst du ihn mit der XPipe SSH-Bridge verbinden. Das kann automatisch funktionieren, sobald der lokale Bridge-Ssh-Schlüssel zu Termius hinzugefügt wurde. + +Das Einzige, was du manuell tun musst, ist, die private Schlüsseldatei `%s` zuerst zu Termius hinzuzufügen. \ No newline at end of file diff --git a/lang/app/texts/termiusSetup_en.md b/lang/app/texts/termiusSetup_en.md new file mode 100644 index 00000000..f4ec9015 --- /dev/null +++ b/lang/app/texts/termiusSetup_en.md @@ -0,0 +1,5 @@ +# Termius setup + +To use Termius as your terminal, you can connect it to the XPipe SSH bridge. This can work automatically once the local bridge ssh key has been added to Termius. + +The only thing you have to do manually is to add the private key file `%s` to Termius first. \ No newline at end of file diff --git a/lang/app/texts/termiusSetup_es.md b/lang/app/texts/termiusSetup_es.md new file mode 100644 index 00000000..c248ee18 --- /dev/null +++ b/lang/app/texts/termiusSetup_es.md @@ -0,0 +1,5 @@ +# Configuración de Termius + +Para utilizar Termius como terminal, puedes conectarlo al puente SSH de XPipe. Esto puede funcionar automáticamente una vez que se haya añadido a Termius la clave ssh del puente local. + +Lo único que tienes que hacer manualmente es añadir primero el archivo de clave privada `%s` a Termius. \ No newline at end of file diff --git a/lang/app/texts/termiusSetup_fr.md b/lang/app/texts/termiusSetup_fr.md new file mode 100644 index 00000000..2b11f7aa --- /dev/null +++ b/lang/app/texts/termiusSetup_fr.md @@ -0,0 +1,5 @@ +# Installation de Termius + +Pour utiliser Termius comme terminal, tu peux le connecter au pont SSH de XPipe. Cela peut fonctionner automatiquement une fois que la clé ssh du pont local a été ajoutée à Termius. + +La seule chose que tu dois faire manuellement est d'ajouter le fichier de clé privée `%s` à Termius d'abord. \ No newline at end of file diff --git a/lang/app/texts/termiusSetup_it.md b/lang/app/texts/termiusSetup_it.md new file mode 100644 index 00000000..7ed2c6ca --- /dev/null +++ b/lang/app/texts/termiusSetup_it.md @@ -0,0 +1,5 @@ +# Configurazione di Termius + +Per utilizzare Termius come terminale, puoi collegarlo al bridge SSH di XPipe. Questo può funzionare automaticamente una volta che la chiave ssh del bridge locale è stata aggiunta a Termius. + +L'unica cosa che devi fare manualmente è aggiungere il file della chiave privata `%s` a Termius. \ No newline at end of file diff --git a/lang/app/texts/termiusSetup_ja.md b/lang/app/texts/termiusSetup_ja.md new file mode 100644 index 00000000..21598b1c --- /dev/null +++ b/lang/app/texts/termiusSetup_ja.md @@ -0,0 +1,5 @@ +# テルミウスのセットアップ + +Termiusをターミナルとして使用するには、XPipe SSHブリッジに接続する。ローカルブリッジのsshキーがTermiusに追加されれば自動的に動作する。 + +唯一手動で行う必要があるのは、最初に秘密鍵ファイル`%s`をTermiusに追加することだ。 \ No newline at end of file diff --git a/lang/app/texts/termiusSetup_nl.md b/lang/app/texts/termiusSetup_nl.md new file mode 100644 index 00000000..8d48cf67 --- /dev/null +++ b/lang/app/texts/termiusSetup_nl.md @@ -0,0 +1,5 @@ +# Termius installatie + +Om Termius als terminal te gebruiken, kun je het verbinden met de XPipe SSH bridge. Dit kan automatisch werken zodra de lokale bridge ssh sleutel is toegevoegd aan Termius. + +Het enige dat je handmatig moet doen is eerst het private key bestand `%s` toevoegen aan Termius. \ No newline at end of file diff --git a/lang/app/texts/termiusSetup_pt.md b/lang/app/texts/termiusSetup_pt.md new file mode 100644 index 00000000..7d35fcb2 --- /dev/null +++ b/lang/app/texts/termiusSetup_pt.md @@ -0,0 +1,5 @@ +# Configuração do Termius + +Para usar o Termius como terminal, podes ligá-lo à bridge SSH XPipe. Isto pode funcionar automaticamente uma vez que a chave ssh da ponte local tenha sido adicionada ao Termius. + +A única coisa que tens de fazer manualmente é adicionar o ficheiro de chave privada `%s` ao Termius primeiro. \ No newline at end of file diff --git a/lang/app/texts/termiusSetup_ru.md b/lang/app/texts/termiusSetup_ru.md new file mode 100644 index 00000000..2b58dbaa --- /dev/null +++ b/lang/app/texts/termiusSetup_ru.md @@ -0,0 +1,5 @@ +# Настройка Термиуса + +Чтобы использовать Termius в качестве терминала, ты можешь подключить его к SSH-мосту XPipe. Это может работать автоматически, как только локальный ssh-ключ моста будет добавлен в Termius. + +Единственное, что тебе придется сделать вручную, - это сначала добавить в Termius файл закрытого ключа `%s`. \ No newline at end of file diff --git a/lang/app/texts/termiusSetup_tr.md b/lang/app/texts/termiusSetup_tr.md new file mode 100644 index 00000000..bf137338 --- /dev/null +++ b/lang/app/texts/termiusSetup_tr.md @@ -0,0 +1,5 @@ +# Termius kurulumu + +Termius'u terminaliniz olarak kullanmak için XPipe SSH köprüsüne bağlayabilirsiniz. Yerel köprü ssh anahtarı Termius'a eklendiğinde bu otomatik olarak çalışabilir. + +Manuel olarak yapmanız gereken tek şey, önce `%s` özel anahtar dosyasını Termius'a eklemektir. \ No newline at end of file diff --git a/lang/app/texts/termiusSetup_zh.md b/lang/app/texts/termiusSetup_zh.md new file mode 100644 index 00000000..8519bb83 --- /dev/null +++ b/lang/app/texts/termiusSetup_zh.md @@ -0,0 +1,5 @@ +# Termius 设置 + +要将 Termius 作为终端使用,可以将其连接到 XPipe SSH 网桥。一旦本地网桥的 ssh 密钥添加到 Termius,它就会自动运行。 + +唯一需要手动操作的是先将私钥文件 `%s` 添加到 Termius。 \ No newline at end of file diff --git a/lang/app/texts/xshellSetup_da.md b/lang/app/texts/xshellSetup_da.md new file mode 100644 index 00000000..e92032ce --- /dev/null +++ b/lang/app/texts/xshellSetup_da.md @@ -0,0 +1,5 @@ +# Xshell setup + +For at bruge Xshell som din terminal kan du forbinde den til XPipe SSH-broen. Det kan ske automatisk, når den lokale ssh-nøgle til broen er blevet føjet til Xshell med det korrekte navn. + +Det eneste, du skal gøre manuelt, er at tilføje den private nøglefil `%s` til Xshell med det faste navn `%s`. \ No newline at end of file diff --git a/lang/app/texts/xshellSetup_de.md b/lang/app/texts/xshellSetup_de.md new file mode 100644 index 00000000..5bfdf539 --- /dev/null +++ b/lang/app/texts/xshellSetup_de.md @@ -0,0 +1,5 @@ +# Xshell-Einrichtung + +Um Xshell als Terminal zu verwenden, kannst du es mit der XPipe SSH-Bridge verbinden. Das kann automatisch funktionieren, sobald der lokale Bridge-Ssh-Schlüssel mit dem richtigen Namen zu Xshell hinzugefügt wurde. + +Das Einzige, was du manuell tun musst, ist, die private Schlüsseldatei `%s` mit dem festen Namen `%s` zu Xshell hinzuzufügen. \ No newline at end of file diff --git a/lang/app/texts/xshellSetup_en.md b/lang/app/texts/xshellSetup_en.md new file mode 100644 index 00000000..9024806e --- /dev/null +++ b/lang/app/texts/xshellSetup_en.md @@ -0,0 +1,5 @@ +# Xshell setup + +To use Xshell as your terminal, you can connect it to the XPipe SSH bridge. This can work automatically once the local bridge ssh key has been added to Xshell with the correct name. + +The only thing you have to do manually is to add the private key file `%s` to Xshell with the fixed name `%s`. \ No newline at end of file diff --git a/lang/app/texts/xshellSetup_es.md b/lang/app/texts/xshellSetup_es.md new file mode 100644 index 00000000..9131ce50 --- /dev/null +++ b/lang/app/texts/xshellSetup_es.md @@ -0,0 +1,5 @@ +# Configuración de Xshell + +Para utilizar Xshell como terminal, puedes conectarlo al puente SSH de XPipe. Esto puede funcionar automáticamente una vez que la clave ssh del puente local se haya añadido a Xshell con el nombre correcto. + +Lo único que tienes que hacer manualmente es añadir el archivo de clave privada `%s` a Xshell con el nombre fijo `%s`. \ No newline at end of file diff --git a/lang/app/texts/xshellSetup_fr.md b/lang/app/texts/xshellSetup_fr.md new file mode 100644 index 00000000..310e3ce6 --- /dev/null +++ b/lang/app/texts/xshellSetup_fr.md @@ -0,0 +1,5 @@ +# Configuration de Xshell + +Pour utiliser Xshell comme terminal, tu peux le connecter au pont SSH de XPipe. Cela peut fonctionner automatiquement une fois que la clé ssh du pont local a été ajoutée à Xshell avec le nom correct. + +La seule chose que tu dois faire manuellement est d'ajouter le fichier de clé privée `%s` à Xshell avec le nom fixe `%s`. \ No newline at end of file diff --git a/lang/app/texts/xshellSetup_it.md b/lang/app/texts/xshellSetup_it.md new file mode 100644 index 00000000..67f20b92 --- /dev/null +++ b/lang/app/texts/xshellSetup_it.md @@ -0,0 +1,5 @@ +# Configurazione di Xshell + +Per utilizzare Xshell come terminale, puoi collegarlo al bridge SSH di XPipe. Questo può funzionare automaticamente una volta che la chiave ssh del bridge locale è stata aggiunta a Xshell con il nome corretto. + +L'unica cosa che devi fare manualmente è aggiungere il file della chiave privata `%s` a Xshell con il nome fisso `%s`. \ No newline at end of file diff --git a/lang/app/texts/xshellSetup_ja.md b/lang/app/texts/xshellSetup_ja.md new file mode 100644 index 00000000..5c98b2bf --- /dev/null +++ b/lang/app/texts/xshellSetup_ja.md @@ -0,0 +1,5 @@ +# Xshellのセットアップ + +Xshellをターミナルとして使用するには、XPipe SSHブリッジに接続する。ローカルブリッジのsshキーが正しい名前でXshellに追加されれば、自動的に動作する。 + +手動で行う必要があるのは、秘密鍵ファイル`%s`を固定名`%s`でXshellに追加することだけだ。 \ No newline at end of file diff --git a/lang/app/texts/xshellSetup_nl.md b/lang/app/texts/xshellSetup_nl.md new file mode 100644 index 00000000..39ac19aa --- /dev/null +++ b/lang/app/texts/xshellSetup_nl.md @@ -0,0 +1,5 @@ +# Xshell instelling + +Om Xshell als terminal te gebruiken kun je het verbinden met de XPipe SSH bridge. Dit kan automatisch werken zodra de lokale bridge ssh key is toegevoegd aan Xshell met de juiste naam. + +Het enige dat je handmatig moet doen is het private key bestand `%s` toevoegen aan Xshell met de vaste naam `%s`. \ No newline at end of file diff --git a/lang/app/texts/xshellSetup_pt.md b/lang/app/texts/xshellSetup_pt.md new file mode 100644 index 00000000..1ab1507d --- /dev/null +++ b/lang/app/texts/xshellSetup_pt.md @@ -0,0 +1,5 @@ +# Configuração do Xshell + +Para utilizar o Xshell como o teu terminal, podes ligá-lo à ponte SSH XPipe. Isto pode funcionar automaticamente quando a chave ssh da ponte local tiver sido adicionada ao Xshell com o nome correto. + +A única coisa que tens de fazer manualmente é adicionar o ficheiro de chave privada `%s` ao Xshell com o nome fixo `%s`. \ No newline at end of file diff --git a/lang/app/texts/xshellSetup_ru.md b/lang/app/texts/xshellSetup_ru.md new file mode 100644 index 00000000..5659fa26 --- /dev/null +++ b/lang/app/texts/xshellSetup_ru.md @@ -0,0 +1,5 @@ +# Xshell setup + +Чтобы использовать Xshell в качестве терминала, ты можешь подключить его к SSH-мосту XPipe. Это может работать автоматически, как только локальный ssh-ключ моста будет добавлен в Xshell с правильным именем. + +Единственное, что тебе придется сделать вручную, - это добавить файл закрытого ключа `%s` в Xshell с фиксированным именем `%s`. \ No newline at end of file diff --git a/lang/app/texts/xshellSetup_tr.md b/lang/app/texts/xshellSetup_tr.md new file mode 100644 index 00000000..18431287 --- /dev/null +++ b/lang/app/texts/xshellSetup_tr.md @@ -0,0 +1,5 @@ +# Xshell kurulumu + +Xshell'i terminaliniz olarak kullanmak için XPipe SSH köprüsüne bağlayabilirsiniz. Yerel köprü ssh anahtarı Xshell'e doğru isimle eklendiğinde bu otomatik olarak çalışabilir. + +Manuel olarak yapmanız gereken tek şey `%s` özel anahtar dosyasını `%s` sabit adıyla Xshell'e eklemektir. \ No newline at end of file diff --git a/lang/app/texts/xshellSetup_zh.md b/lang/app/texts/xshellSetup_zh.md new file mode 100644 index 00000000..1dbed47f --- /dev/null +++ b/lang/app/texts/xshellSetup_zh.md @@ -0,0 +1,5 @@ +# Xshell 设置 + +要将 Xshell 用作终端,可以将其连接到 XPipe SSH 网桥。一旦本地桥接器的 ssh 密钥以正确的名称添加到 Xshell,它就会自动运行。 + +唯一需要手动操作的是将私钥文件 `%s` 添加到 Xshell,并使用固定名称 `%s`。 \ No newline at end of file