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
......@@ -1140,17 +1140,33 @@ const Daemon = struct {
11401140
11411141 const cmd = payload;
11421142
1143- // Daemon appends the task marker so we know when a task is done with
1144- // exit status
1145- const marker = "echo ZMX_TASK_COMPLETED:$?\r";
1146-
1143+ // Redirect stdin from /dev/null to prevent interactive programs
1144+ // (pagers, editors, prompts) from blocking the task. Programs that
1145+ // detect a TTY and open a pager (e.g. git, man) or read stdin
1146+ // (e.g. cat, head) will receive EOF instead of blocking. This
1147+ // matches the behavior of CI runners and `tmux send-keys`.
1148+ //
1149+ // Commands that legitimately need stdin should use `zmx send`
1150+ // instead, or pipe data directly: `echo data | zmx run dev cat`.
1151+ // Here-documents still work because the shell processes the
1152+ // here-document before applying the /dev/null redirection.
1153+ const stdin_redirect = "< /dev/null ";
1154+
1155+ // Chain the exit marker with `;` on the same line. `$?` captures the
1156+ // exit code of the command (not the `;`). The sole exception is when
1157+ // the command contains a heredoc (`<<`) — the delimiter must be alone
1158+ // on its line, so the marker goes on the next line instead.
1159+ const single_line_marker = "; echo ZMX_TASK_COMPLETED:$?\r";
1160+ const heredoc_marker = "\r\necho ZMX_TASK_COMPLETED:$?\r";
1161+ const uses_heredoc = std.mem.indexOf(u8, cmd, "<<") != null;
1162+
1163+ self.queuePtyInput(stdin_redirect);
11471164 if (cmd.len > 0 and cmd[cmd.len - 1] == '\r') {
11481165 self.queuePtyInput(cmd[0 .. cmd.len - 1]);
11491166 } else {
11501167 self.queuePtyInput(cmd);
11511168 }
1152- self.queuePtyInput("\r");
1153- self.queuePtyInput(marker);
1169+ self.queuePtyInput(if (uses_heredoc) heredoc_marker else single_line_marker);
11541170
11551171 try ipc.appendMessage(self.alloc, &client.write_buf, .Ack, "");
11561172 client.has_pending_output = true;
......@@ -1282,13 +1298,10 @@ fn help() !void {
12821298 \\ Commands run inside a PTY using bash
12831299 \\ Commands are passed as-is: do not wrap in quotes.
12841300 \\ Commands run sequentially: do not send multiple in parallel.
1285- \\ Avoid interactive programs (pagers, editors, prompts): they hang.
1286- \\
1287- \\ If the command hangs, send Ctrl+C to recover:
1288- \\ zmx run <session> $(printf '\x03')
1289- \\
1290- \\ If the command hangs, print the history to see the error:
1291- \\ zmx history <session> | tail -100
1301+ \\ Stdin is redirected from /dev/null to prevent interactive programs
1302+ \\ (pagers, editors, prompts) from blocking. Use `zmx send` for
1303+ \\ commands that need user input, or pipe data directly:
1304+ \\ echo "data" | zmx run dev cat
12921305 \\
12931306 \\ `-d` will detach from the calling terminal. Use `wait` to track
12941307 \\ its status.
......@@ -1297,7 +1310,8 @@ fn help() !void {
12971310 \\ zmx run dev ls
12981311 \\ zmx run dev zig build
12991312 \\ zmx run dev grep -r TODO src
1300- \\ zmx run dev git -c core.pager=cat diff
1313+ \\ zmx run dev git log --oneline # pager won't block
1314+ \\ echo "hello" | zmx run dev cat # piped stdin still works
13011315 \\
13021316 \\ zmx run dev -d sleep 10
13031317 \\ zmx wait dev