This commit is contained in:
crschnick 2025-04-04 11:33:06 +00:00
parent e22b66f2a0
commit 120a463d88
9 changed files with 191 additions and 55 deletions

View file

@ -1,6 +1,7 @@
package io.xpipe.app.terminal;
import io.xpipe.app.util.CommandSupport;
import io.xpipe.app.util.ScriptHelper;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellScript;
import io.xpipe.core.process.TerminalInitScriptConfig;
@ -27,16 +28,20 @@ public class ScreenTerminalMultiplexer implements TerminalMultiplexer {
@Override
public ShellScript launchScriptExternal(ShellControl control, String command, TerminalInitScriptConfig config)
throws Exception {
// Screen has a limit of 100 chars for commands
var effectiveCommand = command.length() > 90 ? ScriptHelper.createExecScript(control, command).toString() : command;
return ShellScript.lines("screen -S xpipe -X screen -t \"" + escape(config.getDisplayName(), true) + "\" "
+ escape(command, false));
+ escape(effectiveCommand, false));
}
@Override
public ShellScript launchScriptSession(ShellControl control, String command, TerminalInitScriptConfig config)
throws Exception {
// Screen has a limit of 100 chars for commands
var effectiveCommand = command.length() > 90 ? ScriptHelper.createExecScript(control, command).toString() : command;
return ShellScript.lines(
"for scr in $(screen -ls | grep xpipe | awk '{print $1}'); do screen -S $scr -X quit; done",
"screen -S xpipe -t \"" + escape(config.getDisplayName(), true) + "\" " + escape(command, false));
"screen -S xpipe -t \"" + escape(config.getDisplayName(), true) + "\" " + escape(effectiveCommand, false));
}
private String escape(String s, boolean quotes) {

View file

@ -18,7 +18,10 @@ public class TerminalDockModel {
public synchronized void trackTerminal(ControllableTerminalSession terminal) {
terminalInstances.add(terminal);
terminal.alwaysInFront();
// The main window always loses focus when the terminal is opened,
// so only put it in front
// If we refocus the main window, it will get put always in front then
terminal.frontOfMainWindow();
if (viewBounds != null) {
terminal.updatePosition(viewBounds);
}

View file

@ -38,7 +38,7 @@ public class TmuxTerminalMultiplexer implements TerminalMultiplexer {
"tmux kill-session -t xpipe",
"tmux new-session -d -s xpipe",
"tmux rename-window \"" + escape(config.getDisplayName(), true) + "\"",
"tmux send-keys -t xpipe '" + escape(command, false) + "' Enter",
"tmux send-keys -t xpipe '" + escape(command, false) + ";exit' Enter",
"tmux attach -d -t xpipe");
}

View file

@ -30,7 +30,7 @@ public class ZellijTerminalMultiplexer implements TerminalMultiplexer {
return ShellScript.lines(
"zellij attach --create-background xpipe",
"zellij -s xpipe action new-tab --name \"" + escape(config.getDisplayName(), false, true) + "\"",
"zellij -s xpipe action write-chars -- " + escape(command, true, true),
"zellij -s xpipe action write-chars -- " + escape(command, true, true) + "\\;exit",
"zellij -s xpipe action write 10");
}
@ -40,7 +40,7 @@ public class ZellijTerminalMultiplexer implements TerminalMultiplexer {
return ShellScript.lines(
"zellij delete-session -f xpipe",
"zellij attach --create-background xpipe",
"zellij -s xpipe run --name \"" + escape(config.getDisplayName(), false, true) + "\" -- "
"zellij -s xpipe run -c --name \"" + escape(config.getDisplayName(), false, true) + "\" -- "
+ escape(command, false, false),
"zellij attach xpipe");
}

View file

@ -0,0 +1,68 @@
package io.xpipe.app.util;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.core.process.CommandControl;
import io.xpipe.core.process.ProcessOutputException;
import javafx.scene.control.TextArea;
import javafx.scene.layout.StackPane;
import org.apache.commons.lang3.exception.ExceptionUtils;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
public class CommandDialog {
public static void runAsyncAndShow(CommandControl cmd) {
ThreadHelper.runAsync(() -> {
run(cmd);
});
}
private static void run(CommandControl cmd) {
String out;
try {
out = cmd.readStdoutOrThrow();
if (out.isEmpty()) {
out = "<empty>";
}
if (out.length() > 10000) {
var counter = new AtomicInteger();
var start = out.lines()
.filter(s -> {
counter.incrementAndGet();
return true;
})
.limit(100)
.collect(Collectors.joining("\n"));
var notShownLines = counter.get() - 100;
if (notShownLines > 0) {
out = start + "\n\n... " + notShownLines + " more lines";
} else {
out = start;
}
}
} catch (ProcessOutputException e) {
out = e.getMessage();
} catch (Throwable t) {
out = ExceptionUtils.getStackTrace(t);
}
String finalOut = out;
var modal = ModalOverlay.of(
"commandOutput",
Comp.of(() -> {
var text = new TextArea(finalOut);
text.setWrapText(true);
text.setEditable(false);
text.setPrefRowCount(Math.max(8, (int)
finalOut.lines().count()));
var sp = new StackPane(text);
return sp;
})
.prefWidth(650));
modal.show();
}
}

View file

@ -8,6 +8,7 @@ import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.terminal.TerminalLauncher;
import io.xpipe.app.util.CommandDialog;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.core.process.ShellTtyState;
import io.xpipe.core.process.SystemState;
@ -63,7 +64,7 @@ public class RunScriptActionMenu implements ActionProvider {
@Override
public LabelGraphic getIcon(DataStoreEntryRef<ShellStore> store) {
return new LabelGraphic.IconGraphic("mdi2d-desktop-mac");
return new LabelGraphic.IconGraphic("mdi2c-code-greater-than");
}
@Override
@ -87,7 +88,7 @@ public class RunScriptActionMenu implements ActionProvider {
@Override
public String getIcon() {
return "mdi2d-desktop-mac";
return "mdi2c-code-greater-than";
}
@Override
@ -108,6 +109,78 @@ public class RunScriptActionMenu implements ActionProvider {
}
}
@Value
private static class HubRunActionProvider implements ActionProvider {
ScriptHierarchy hierarchy;
@Value
private class Action implements ActionProvider.Action {
DataStoreEntryRef<ShellStore> shellStore;
@Override
public void execute() throws Exception {
var sc = shellStore.getStore().getOrStartSession();
var script = hierarchy.getLeafBase().getStore().assembleScriptChain(sc);
var cmd = sc.command(script);
CommandDialog.runAsyncAndShow(cmd);
}
}
@Override
public LeafDataStoreCallSite<?> getLeafDataStoreCallSite() {
return new LeafDataStoreCallSite<ShellStore>() {
@Override
public Action createAction(DataStoreEntryRef<ShellStore> store) {
return new Action(store);
}
@Override
public ObservableValue<String> getName(DataStoreEntryRef<ShellStore> store) {
return AppI18n.observable("runInConnectionHub");
}
@Override
public LabelGraphic getIcon(DataStoreEntryRef<ShellStore> store) {
return new LabelGraphic.IconGraphic("mdi2d-desktop-mac");
}
@Override
public Class<?> getApplicableClass() {
return ShellStore.class;
}
};
}
@Override
public BatchDataStoreCallSite<ShellStore> getBatchDataStoreCallSite() {
return new BatchDataStoreCallSite<ShellStore>() {
@Override
public ObservableValue<String> getName() {
return AppI18n.observable("runInConnectionHub");
}
@Override
public String getIcon() {
return "mdi2d-desktop-mac";
}
@Override
public Class<?> getApplicableClass() {
return ShellStore.class;
}
@Override
public ActionProvider.Action createAction(DataStoreEntryRef<ShellStore> store) {
return new Action(store);
}
};
}
}
@Value
private static class BackgroundRunActionProvider implements ActionProvider {
@ -215,7 +288,7 @@ public class RunScriptActionMenu implements ActionProvider {
@Override
public List<? extends ActionProvider> getChildren(DataStoreEntryRef<ShellStore> store) {
return List.of(
new TerminalRunActionProvider(hierarchy), new BackgroundRunActionProvider(hierarchy));
new TerminalRunActionProvider(hierarchy), new HubRunActionProvider(hierarchy), new BackgroundRunActionProvider(hierarchy));
}
};
}
@ -284,7 +357,7 @@ public class RunScriptActionMenu implements ActionProvider {
public List<? extends ActionProvider> getChildren(List<DataStoreEntryRef<ShellStore>> batch) {
if (hierarchy.isLeaf()) {
return List.of(
new TerminalRunActionProvider(hierarchy), new BackgroundRunActionProvider(hierarchy));
new TerminalRunActionProvider(hierarchy), new HubRunActionProvider(hierarchy), new BackgroundRunActionProvider(hierarchy));
}
return hierarchy.getChildren().stream()

View file

@ -6,6 +6,7 @@ import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.CommandDialog;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.ShellControl;
@ -61,6 +62,31 @@ public abstract class MultiExecuteAction implements BrowserBranchAction {
return AppPrefs.get().terminalType().getValue() != null;
}
},
new BrowserLeafAction() {
@Override
public void execute(BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
model.withShell(
pc -> {
for (BrowserEntry entry : entries) {
var c = createCommand(pc, model, entry);
if (c == null) {
return;
}
var cmd = pc.command(c);
CommandDialog.runAsyncAndShow(cmd);
}
},
true);
}
@Override
public ObservableValue<String> getName(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return AppI18n.observable("runInFileBrowser");
}
},
new BrowserLeafAction() {
@Override
@ -85,7 +111,7 @@ public abstract class MultiExecuteAction implements BrowserBranchAction {
@Override
public ObservableValue<String> getName(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return AppI18n.observable("executeInBackground");
return AppI18n.observable("runSilent");
}
});
}

View file

@ -9,6 +9,7 @@ import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.CommandDialog;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.ProcessOutputException;
import io.xpipe.core.process.ShellControl;
@ -82,48 +83,7 @@ public abstract class MultiExecuteSelectionAction implements BrowserBranchAction
}
var cmd = pc.command(c);
String out;
try {
out = cmd.readStdoutOrThrow();
if (out.isEmpty()) {
out = "<empty>";
}
if (out.length() > 10000) {
var counter = new AtomicInteger();
var start = out.lines()
.filter(s -> {
counter.incrementAndGet();
return true;
})
.limit(100)
.collect(Collectors.joining("\n"));
var notShownLines = counter.get() - 100;
if (notShownLines > 0) {
out = start + "\n\n... " + notShownLines + " more lines";
} else {
out = start;
}
}
} catch (ProcessOutputException e) {
out = e.getMessage();
}
String finalOut = out;
var modal = ModalOverlay.of(
"commandOutput",
Comp.of(() -> {
var text = new TextArea(finalOut);
text.setWrapText(true);
text.setEditable(false);
text.setPrefRowCount(Math.max(8, (int)
finalOut.lines().count()));
var sp = new StackPane(text);
return sp;
})
.prefWidth(650));
modal.show();
CommandDialog.runAsyncAndShow(cmd);
},
true);
}
@ -131,7 +91,7 @@ public abstract class MultiExecuteSelectionAction implements BrowserBranchAction
@Override
public ObservableValue<String> getName(
BrowserFileSystemTabModel model, List<BrowserEntry> entries) {
return AppI18n.observable("runCommand");
return AppI18n.observable("runInFileBrowser");
}
},
new BrowserLeafAction() {

View file

@ -1333,7 +1333,8 @@ iconDirectory=Icon directory
addUnsupportedKexMethod=Add unsupported key exchange method
addUnsupportedKexMethodDescription=Allow the key exchange method to be used for this connection
runSilent=silently in background
runCommand=in file browser
runInFileBrowser=in file browser
runInConnectionHub=in connection hub
commandOutput=Command output
iconSourceDeletionTitle=Delete icon source
iconSourceDeletionContent=Do you want to delete this icon source and all associated icons with it?