Commit a25e2fe

Eric Bower  ·  2026-05-20 22:21:35 -0400 EDT
parent d5c6926
refactor(run): marker for heredoc

For most commands we can append `; $?` except for heredoc where the marker
needs to be on its own line.
1 files changed,  +28, -14
+28, -14
 1@@ -1140,17 +1140,33 @@ const Daemon = struct {
 2 
 3         const cmd = payload;
 4 
 5-        // Daemon appends the task marker so we know when a task is done with
 6-        // exit status
 7-        const marker = "echo ZMX_TASK_COMPLETED:$?\r";
 8-
 9+        // Redirect stdin from /dev/null to prevent interactive programs
10+        // (pagers, editors, prompts) from blocking the task. Programs that
11+        // detect a TTY and open a pager (e.g. git, man) or read stdin
12+        // (e.g. cat, head) will receive EOF instead of blocking. This
13+        // matches the behavior of CI runners and `tmux send-keys`.
14+        //
15+        // Commands that legitimately need stdin should use `zmx send`
16+        // instead, or pipe data directly: `echo data | zmx run dev cat`.
17+        // Here-documents still work because the shell processes the
18+        // here-document before applying the /dev/null redirection.
19+        const stdin_redirect = "< /dev/null ";
20+
21+        // Chain the exit marker with `;` on the same line. `$?` captures the
22+        // exit code of the command (not the `;`). The sole exception is when
23+        // the command contains a heredoc (`<<`) — the delimiter must be alone
24+        // on its line, so the marker goes on the next line instead.
25+        const single_line_marker = "; echo ZMX_TASK_COMPLETED:$?\r";
26+        const heredoc_marker = "\r\necho ZMX_TASK_COMPLETED:$?\r";
27+        const uses_heredoc = std.mem.indexOf(u8, cmd, "<<") != null;
28+
29+        self.queuePtyInput(stdin_redirect);
30         if (cmd.len > 0 and cmd[cmd.len - 1] == '\r') {
31             self.queuePtyInput(cmd[0 .. cmd.len - 1]);
32         } else {
33             self.queuePtyInput(cmd);
34         }
35-        self.queuePtyInput("\r");
36-        self.queuePtyInput(marker);
37+        self.queuePtyInput(if (uses_heredoc) heredoc_marker else single_line_marker);
38 
39         try ipc.appendMessage(self.alloc, &client.write_buf, .Ack, "");
40         client.has_pending_output = true;
41@@ -1282,13 +1298,10 @@ fn help() !void {
42         \\  Commands run inside a PTY using bash
43         \\  Commands are passed as-is: do not wrap in quotes.
44         \\  Commands run sequentially: do not send multiple in parallel.
45-        \\  Avoid interactive programs (pagers, editors, prompts): they hang.
46-        \\
47-        \\  If the command hangs, send Ctrl+C to recover:
48-        \\    zmx run <session> $(printf '\x03')
49-        \\
50-        \\  If the command hangs, print the history to see the error:
51-        \\    zmx history <session> | tail -100
52+        \\  Stdin is redirected from /dev/null to prevent interactive programs
53+        \\  (pagers, editors, prompts) from blocking. Use `zmx send` for
54+        \\  commands that need user input, or pipe data directly:
55+        \\    echo "data" | zmx run dev cat
56         \\
57         \\  `-d` will detach from the calling terminal. Use `wait` to track
58         \\  its status.
59@@ -1297,7 +1310,8 @@ fn help() !void {
60         \\    zmx run dev ls
61         \\    zmx run dev zig build
62         \\    zmx run dev grep -r TODO src
63-        \\    zmx run dev git -c core.pager=cat diff
64+        \\    zmx run dev git log --oneline          # pager won't block
65+        \\    echo "hello" | zmx run dev cat         # piped stdin still works
66         \\
67         \\    zmx run dev -d sleep 10
68         \\    zmx wait dev