Rework file paths and kitty support

This commit is contained in:
crschnick 2024-04-01 03:43:22 +00:00
parent c228e6ba30
commit a8f07c3e0f
13 changed files with 130 additions and 68 deletions

View file

@ -60,6 +60,7 @@ public class TroubleshootCategory extends AppPrefsCategory {
"openCurrentLogFileDescription",
"mdmz-text_snippet",
e -> {
AppLogs.get().flush();
FileOpener.openInTextEditor(AppLogs.get()
.getSessionLogsDirectory()
.resolve("xpipe.log")

View file

@ -10,6 +10,7 @@ import io.xpipe.app.util.LocalShell;
import io.xpipe.app.util.MacOsPermissions;
import io.xpipe.app.util.WindowsRegistry;
import io.xpipe.core.process.*;
import io.xpipe.core.store.FilePath;
import lombok.Getter;
import lombok.Value;
import lombok.With;
@ -131,7 +132,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
.add("-e")
.add("cmd")
.add("/c")
.addQuoted(configuration.getScriptFile().replaceAll(" ", "^$0"));
.addQuoted(configuration.getScriptFile().toString().replaceAll(" ", "^$0"));
}
};
ExternalTerminalType TABBY_WINDOWS = new WindowsType("app.tabby", "Tabby") {
@ -311,28 +312,12 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
return CommandBuilder.of()
.add("-e")
.addQuoted(configuration.getScriptFile())
.addFile(configuration.getScriptFile())
.add("-T")
.addQuoted(configuration.getColoredTitle())
.add("--new-tab");
}
};
ExternalTerminalType KITTY_LINUX = new SimplePathType("app.kitty", "kitty") {
@Override
public boolean supportsTabs() {
return false;
}
@Override
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
return CommandBuilder.of()
.add("-1")
.add("-T")
.addQuoted(configuration.getColoredTitle())
.addQuoted(configuration.getScriptFile());
}
};
ExternalTerminalType TERMINOLOGY = new SimplePathType("app.terminology", "terminology") {
@Override
@ -347,7 +332,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
.addQuoted(configuration.getColoredTitle())
.add("-2")
.add("-e")
.addQuoted(configuration.getScriptFile());
.addFile(configuration.getScriptFile());
}
};
ExternalTerminalType COOL_RETRO_TERM = new SimplePathType("app.coolRetroTerm", "cool-retro-term") {
@ -363,7 +348,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
.add("-T")
.addQuoted(configuration.getColoredTitle())
.add("-e")
.addQuoted(configuration.getScriptFile());
.addFile(configuration.getScriptFile());
}
};
ExternalTerminalType GUAKE = new SimplePathType("app.guake", "guake") {
@ -380,7 +365,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
.add("-r")
.addQuoted(configuration.getColoredTitle())
.add("-e")
.addQuoted(configuration.getScriptFile());
.addFile(configuration.getScriptFile());
}
};
ExternalTerminalType ALACRITTY_LINUX = new SimplePathType("app.alacritty", "alacritty") {
@ -401,7 +386,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
.add("-t")
.addQuoted(configuration.getCleanTitle())
.add("-e")
.addQuoted(configuration.getScriptFile());
.addFile(configuration.getScriptFile());
}
};
ExternalTerminalType TILDA = new SimplePathType("app.tilda", "tilda") {
@ -413,7 +398,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
@Override
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
return CommandBuilder.of().add("-c").addQuoted(configuration.getScriptFile());
return CommandBuilder.of().add("-c").addFile(configuration.getScriptFile());
}
};
ExternalTerminalType XTERM = new SimplePathType("app.xterm", "xterm") {
@ -429,7 +414,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
.add("-title")
.addQuoted(configuration.getColoredTitle())
.add("-e")
.addQuoted(configuration.getScriptFile());
.addFile(configuration.getScriptFile());
}
};
ExternalTerminalType DEEPIN_TERMINAL = new SimplePathType("app.deepinTerminal", "deepin-terminal") {
@ -441,7 +426,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
@Override
protected CommandBuilder toCommand(LaunchConfiguration configuration) {
return CommandBuilder.of().add("-C").addQuoted(configuration.getScriptFile());
return CommandBuilder.of().add("-C").addFile(configuration.getScriptFile());
}
};
ExternalTerminalType Q_TERMINAL = new SimplePathType("app.qTerminal", "qterminal") {
@ -465,7 +450,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
@Override
public void launch(LaunchConfiguration configuration) throws Exception {
try (ShellControl pc = LocalShell.getShell()) {
var suffix = "\"" + configuration.getScriptFile().replaceAll("\"", "\\\\\"") + "\"";
var suffix = "\"" + configuration.getScriptFile().toString().replaceAll("\"", "\\\\\"") + "\"";
pc.osascriptCommand(String.format(
"""
activate application "Terminal"
@ -508,7 +493,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
create window with default profile command "%s"
end tell
""",
a, a, a, a, configuration.getScriptFile().replaceAll("\"", "\\\\\"")))
a, a, a, a, configuration.getScriptFile().toString().replaceAll("\"", "\\\\\"")))
.execute();
}
}
@ -633,7 +618,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
end tell
end tell
""",
configuration.getScriptFile().replaceAll("\"", "\\\\\"")))
configuration.getScriptFile().toString().replaceAll("\"", "\\\\\"")))
.execute();
}
}
@ -656,7 +641,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
GNOME_TERMINAL,
TILIX,
TERMINATOR,
KITTY_LINUX,
KittyTerminalType.KITTY_LINUX,
TERMINOLOGY,
COOL_RETRO_TERM,
GUAKE,
@ -747,12 +732,12 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
String cleanTitle;
@With
String scriptFile;
FilePath scriptFile;
ShellDialect scriptDialect;
public CommandBuilder getDialectLaunchCommand() {
var open = scriptDialect.getOpenScriptCommand(scriptFile);
var open = scriptDialect.getOpenScriptCommand(scriptFile.toString());
return open;
}
@ -783,7 +768,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue {
var format = custom.toLowerCase(Locale.ROOT).contains("$cmd") ? custom : custom + " $CMD";
try (var pc = LocalShell.getShell()) {
var toExecute = ApplicationHelper.replaceFileArgument(format, "CMD", configuration.getScriptFile());
var toExecute = ApplicationHelper.replaceFileArgument(format, "CMD", configuration.getScriptFile().toString());
// We can't be sure whether the command is blocking or not, so always make it not blocking
if (pc.getOsType().equals(OsType.WINDOWS)) {
toExecute = "start \"" + configuration.getCleanTitle() + "\" " + toExecute;

View file

@ -0,0 +1,66 @@
package io.xpipe.app.terminal;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import io.xpipe.app.util.LocalShell;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.util.XPipeInstallation;
public class KittyTerminalType {
public static final ExternalTerminalType KITTY_LINUX = new ExternalTerminalType() {
@Override
public String getId() {
return "app.tabby";
}
@Override
public boolean supportsTabs() {
return true;
}
@Override
public void launch(LaunchConfiguration configuration) throws Exception {
launchInstanceIfNeeded();
open(configuration);
}
};
private static FilePath getSocket() throws Exception {
try (var sc = LocalShell.getShell().start()) {
var temp = sc.getSystemTemporaryDirectory();
return temp.join("xpipe_kitty");
}
}
private static void launchInstanceIfNeeded() throws Exception {
var socket = getSocket();
try (var sc = LocalShell.getShell().start()) {
if (sc.getShellDialect().createFileExistsCommand(sc,socket.toString()).executeAndCheck()) {
return;
}
sc.executeSimpleCommand(CommandBuilder.of().add("kitty").add("-o", "allow_remote_control=socket-only", "--listen-on", "unix:" + getSocket(), "--detach"));
}
}
private static void open(ExternalTerminalType.LaunchConfiguration configuration) throws Exception {
try (var sc = LocalShell.getShell().start()) {
var payload = JsonNodeFactory.instance.objectNode();
payload.put("args", configuration.getDialectLaunchCommand().buildString(sc));
payload.put("tab_title",configuration.getColoredTitle());
payload.put("type", "tab");
payload.put("logo_alpha", 0.01);
payload.put("logo", XPipeInstallation.getLocalDefaultInstallationIcon().toString());
var json = JsonNodeFactory.instance.objectNode();
json.put("cmd", "launch");
json.set("payload", payload);
var jsonString = json.toString();
var echoString = "'\\eP@kitty-cmd" + jsonString + "\\e\\\\'";
sc.executeSimpleCommand(CommandBuilder.of().add("echo", "-en", echoString, "|", "socat", "-").addFile(getSocket()));
}
}
}

View file

@ -75,7 +75,7 @@ public class WindowsTerminalType {
? CommandBuilder.of().addFile(configuration.getScriptFile())
: CommandBuilder.of()
.add("powershell", "-ExecutionPolicy", "Bypass", "-File")
.addQuoted(configuration.getScriptFile());
.addFile(configuration.getScriptFile());
var cmd = CommandBuilder.of().add("-w", "1", "nt");
if (configuration.getColor() != null) {

View file

@ -42,7 +42,7 @@ public class ApplicationHelper {
return String.format(
"Start-Process -FilePath %s -ArgumentList \"-NoProfile\", \"-File\", %s",
pc.getShellDialect().getExecutableName(),
pc.getShellDialect().fileArgument(script));
pc.getShellDialect().fileArgument(script.toString()));
}
if (pc.getOsType().equals(OsType.WINDOWS)) {

View file

@ -2,7 +2,7 @@ package io.xpipe.app.util;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.core.process.*;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.util.FailableFunction;
import io.xpipe.core.util.SecretValue;
import lombok.SneakyThrows;
@ -21,13 +21,13 @@ public class ScriptHelper {
}
@SneakyThrows
public static String createLocalExecScript(String content) {
public static FilePath createLocalExecScript(String content) {
try (var l = LocalShell.getShell().start()) {
return createExecScript(l, content);
}
}
public static String constructTerminalInitFile(
public static FilePath constructTerminalInitFile(
ShellDialect t,
ShellControl processControl,
FailableFunction<ShellControl, String, Exception> workingDirectory,
@ -74,37 +74,37 @@ public class ScriptHelper {
content += nl + t.getPassthroughExitCommand() + nl;
}
return createExecScript(t, processControl, t.initFileName(processControl), content);
return createExecScript(t, processControl, new FilePath(t.initFileName(processControl)), content);
}
@SneakyThrows
public static String getExecScriptFile(ShellControl processControl) {
public static FilePath getExecScriptFile(ShellControl processControl) {
return getExecScriptFile(
processControl, processControl.getShellDialect().getScriptFileEnding());
}
@SneakyThrows
public static String getExecScriptFile(ShellControl processControl, String fileEnding) {
public static FilePath getExecScriptFile(ShellControl processControl, String fileEnding) {
var fileName = "exec-" + getScriptId();
var temp = processControl.getSystemTemporaryDirectory();
return FileNames.join(temp, fileName + "." + fileEnding);
return temp.join(fileName + "." + fileEnding);
}
@SneakyThrows
public static String createExecScript(ShellControl processControl, String content) {
public static FilePath createExecScript(ShellControl processControl, String content) {
return createExecScript(processControl.getShellDialect(), processControl, content);
}
@SneakyThrows
public static String createExecScript(ShellDialect type, ShellControl processControl, String content) {
public static FilePath createExecScript(ShellDialect type, ShellControl processControl, String content) {
var fileName = "exec-" + getScriptId();
var temp = processControl.getSystemTemporaryDirectory();
var file = FileNames.join(temp, fileName + "." + type.getScriptFileEnding());
var file = temp.join(fileName + "." + type.getScriptFileEnding());
return createExecScript(type, processControl, file, content);
}
@SneakyThrows
public static String createExecScript(ShellDialect type, ShellControl processControl, String file, String content) {
public static FilePath createExecScript(ShellDialect type, ShellControl processControl, FilePath file, String content) {
content = type.prepareScriptContent(content);
TrackEvent.withTrace("Writing exec script")
@ -114,12 +114,12 @@ public class ScriptHelper {
processControl
.getShellDialect()
.createScriptTextFileWriteCommand(processControl, content, file)
.createScriptTextFileWriteCommand(processControl, content, file.toString())
.execute();
return file;
}
public static String createRemoteAskpassScript(ShellControl parent, UUID requestId, String prefix)
public static FilePath createRemoteAskpassScript(ShellControl parent, UUID requestId, String prefix)
throws Exception {
var type = parent.getShellDialect();
@ -130,7 +130,7 @@ public class ScriptHelper {
var fileName = "exec-" + getScriptId() + "." + type.getScriptFileEnding();
var temp = parent.getSystemTemporaryDirectory();
var file = FileNames.join(temp, fileName);
var file = temp.join(fileName);
if (type != parent.getShellDialect()) {
try (var sub = parent.subShell(type).start()) {
var content =
@ -144,12 +144,12 @@ public class ScriptHelper {
}
}
public static String createTerminalPreparedAskpassScript(
public static FilePath createTerminalPreparedAskpassScript(
SecretValue pass, ShellControl parent, boolean forceExecutable) throws Exception {
return createTerminalPreparedAskpassScript(pass != null ? List.of(pass) : List.of(), parent, forceExecutable);
}
public static String createTerminalPreparedAskpassScript(
public static FilePath createTerminalPreparedAskpassScript(
List<SecretValue> pass, ShellControl parent, boolean forceExecutable) throws Exception {
var scriptType = parent.getShellDialect();
@ -161,18 +161,18 @@ public class ScriptHelper {
return createTerminalPreparedAskpassScript(pass, parent, scriptType);
}
private static String createTerminalPreparedAskpassScript(
private static FilePath createTerminalPreparedAskpassScript(
List<SecretValue> pass, ShellControl parent, ShellDialect type) throws Exception {
var fileName = "exec-" + getScriptId() + "." + type.getScriptFileEnding();
var temp = parent.getSystemTemporaryDirectory();
var file = FileNames.join(temp, fileName);
var file = temp.join(fileName);
if (type != parent.getShellDialect()) {
try (var sub = parent.subShell(type).start()) {
var content = sub.getShellDialect()
.getAskpass()
.prepareFixedContent(
sub,
file,
file.toString(),
pass.stream()
.map(secretValue -> secretValue.getSecretValue())
.toList());
@ -183,7 +183,7 @@ public class ScriptHelper {
.getAskpass()
.prepareFixedContent(
parent,
file,
file.toString(),
pass.stream()
.map(secretValue -> secretValue.getSecretValue())
.toList());

View file

@ -4,6 +4,7 @@ import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FilePath;
import org.apache.commons.io.FileUtils;
import java.io.IOException;
@ -34,37 +35,37 @@ public class ShellTemp {
return temp.resolve(sub);
}
public static String getUserSpecificTempDataDirectory(ShellControl proc, String sub) throws Exception {
String base;
public static FilePath getUserSpecificTempDataDirectory(ShellControl proc, String sub) throws Exception {
FilePath base;
// On Windows and macOS, we already have user specific temp directories
// Even on macOS as root it is technically unique as only root will use /tmp
if (!proc.getOsType().equals(OsType.WINDOWS) && !proc.getOsType().equals(OsType.MACOS)) {
var temp = proc.getSystemTemporaryDirectory();
base = FileNames.join(temp, "xpipe");
base = temp.join("xpipe");
// We have to make sure that also other users can create files here
// This command should work in all shells
proc.command("chmod 777 " + proc.getShellDialect().fileArgument(base))
.executeAndCheck();
var user = proc.getShellDialect().printUsernameCommand(proc).readStdoutOrThrow();
base = FileNames.join(base, user);
base = temp.join(user);
} else {
var temp = proc.getSystemTemporaryDirectory();
base = FileNames.join(temp, "xpipe");
base = temp.join("xpipe");
}
return FileNames.join(base, sub);
return base.join(sub);
}
public static void checkTempDirectory(ShellControl proc) throws Exception {
var d = proc.getShellDialect();
var systemTemp = proc.getSystemTemporaryDirectory();
if (!d.directoryExists(proc, systemTemp).executeAndCheck() || !checkDirectoryPermissions(proc, systemTemp)) {
if (!d.directoryExists(proc, systemTemp.toString()).executeAndCheck() || !checkDirectoryPermissions(proc, systemTemp.toString())) {
throw ErrorEvent.expected(new IOException("No permissions to access %s".formatted(systemTemp)));
}
// Always delete legacy directory and do not care whether it partially fails
// This system xpipe temp directory might contain other files on the local machine, so only clear the exec
d.deleteFileOrDirectory(proc, FileNames.join(systemTemp, "xpipe", "exec"))
d.deleteFileOrDirectory(proc, systemTemp.join("xpipe", "exec").toString())
.executeAndCheck();
var home = proc.getOsType().getHomeDirectory(proc);
d.deleteFileOrDirectory(proc, FileNames.join(home, ".xpipe", "temp")).executeAndCheck();

View file

@ -36,7 +36,7 @@ public class TerminalLauncherManager {
try {
var file = ScriptHelper.createLocalExecScript(processControl.prepareTerminalOpen(config, workingDirectory));
entry.setResult(new ResultSuccess(Path.of(file)));
entry.setResult(new ResultSuccess(Path.of(file.toString())));
} catch (Exception e) {
entry.setResult(new ResultFailure(e));
}

View file

@ -1,5 +1,6 @@
package io.xpipe.core.process;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.util.FailableConsumer;
import io.xpipe.core.util.FailableFunction;
import lombok.Getter;
@ -185,6 +186,10 @@ public class CommandBuilder {
return this;
}
public CommandBuilder addFile(FilePath s) {
return addFile(shellControl -> shellControl.getShellDialect().fileArgument(s));
}
public CommandBuilder addLiteral(String s) {
elements.add(sc -> {
if (s == null) {

View file

@ -1,5 +1,6 @@
package io.xpipe.core.process;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.store.ShellStore;
import io.xpipe.core.store.StatefulDataStore;
import io.xpipe.core.util.FailableConsumer;
@ -92,7 +93,7 @@ public interface ShellControl extends ProcessControl {
FailableFunction<ShellControl, String, Exception> workingDirectory)
throws Exception;
String getSystemTemporaryDirectory();
FilePath getSystemTemporaryDirectory();
default CommandControl osascriptCommand(String script) {
return command(String.format(

View file

@ -1,6 +1,7 @@
package io.xpipe.core.process;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.store.FileSystem;
import io.xpipe.core.util.NewLine;
import io.xpipe.core.util.SecretValue;
@ -51,6 +52,10 @@ public interface ShellDialect {
String fileArgument(String s);
default String fileArgument(FilePath s) {
return fileArgument(s.toString());
}
String quoteArgument(String s);
String prepareTerminalEnvironmentCommands();

View file

@ -114,9 +114,7 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore,
.mapToInt(value ->
value.get().getName().hashCode() + value.getStore().hashCode())
.sum();
var targetDir = FileNames.join(
ShellTemp.getUserSpecificTempDataDirectory(proc, "scripts"),
proc.getShellDialect().getId());
var targetDir = ShellTemp.getUserSpecificTempDataDirectory(proc, "scripts").join(proc.getShellDialect().getId()).toString();
var hashFile = FileNames.join(targetDir, "hash");
var d = proc.getShellDialect();
if (d.createFileExistsCommand(proc, hashFile).executeAndCheck()) {

View file

@ -39,7 +39,7 @@ public class SimpleScriptStore extends ScriptStore implements ScriptSnippet {
.collect(Collectors.joining(
shellControl.getShellDialect().getNewLine().getNewLineString()));
var script = ScriptHelper.createExecScript(targetType, shellControl, fixedCommands);
return targetType.sourceScriptCommand(shellControl, script);
return targetType.sourceScriptCommand(shellControl, script.toString());
}
return null;