From 65d87acb537f9d4b79ead039fe5dd500f173f368 Mon Sep 17 00:00:00 2001 From: Sebastiaan de Schaetzen Date: Thu, 23 Apr 2026 08:44:13 +0200 Subject: [PATCH] Changed something --- main.go | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 104 insertions(+), 11 deletions(-) diff --git a/main.go b/main.go index fde6a3c..102d85c 100644 --- a/main.go +++ b/main.go @@ -460,8 +460,9 @@ func run(name string, args []string) int { }() // Put stdin in raw mode if it is a terminal. + var oldState *term.State if term.IsTerminal(int(os.Stdin.Fd())) { - oldState, err := term.MakeRaw(int(os.Stdin.Fd())) + oldState, err = term.MakeRaw(int(os.Stdin.Fd())) if err != nil { log.Printf("make raw: %v", err) return 1 @@ -469,19 +470,111 @@ func run(name string, args []string) int { defer term.Restore(int(os.Stdin.Fd()), oldState) } - go io.Copy(ptmx, os.Stdin) + childPid := c.Process.Pid + termState := oldState // updated on each MakeRaw; only touched by suspend goroutine + + var suspendMu sync.Mutex // ensures only one suspend/resume cycle runs at a time + + // Copy stdin → PTY, intercepting Ctrl+Z (0x1A) for job control. + go func() { + buf := make([]byte, 4096) + for { + n, err := os.Stdin.Read(buf) + if n > 0 { + i := 0 + for j := 0; j < n; j++ { + if buf[j] == 0x1A { // Ctrl+Z + if j > i { + ptmx.Write(buf[i:j]) + } + // Run the full suspend/resume cycle in one goroutine so + // there is no cross-goroutine state to synchronize. + go func() { + if !suspendMu.TryLock() { + return // already suspending + } + defer suspendMu.Unlock() + + // Stop the child immediately without forwarding 0x1A. + // Forwarding 0x1A causes copilot to write partial teardown + // sequences that glitch the display before we can stop it. + // We send to both the direct PID and the process group so + // that subprocesses are also stopped regardless of whether + // pty.Start created a new process group. + // SIGSTOP is synchronous — the child is stopped before + // Kill() returns, so no sleep is needed. + syscall.Kill(childPid, syscall.SIGSTOP) + syscall.Kill(-childPid, syscall.SIGSTOP) + + // Exit alternate screen and show cursor in case copilot had + // them active, so the shell prompt appears on a clean screen. + os.Stdout.Write([]byte("\x1b[?1049l\x1b[?25h\r\n")) + // Hand the terminal back to the shell. + if termState != nil { + term.Restore(int(os.Stdin.Fd()), termState) + } + // Stop ourselves; the shell takes over. + syscall.Kill(os.Getpid(), syscall.SIGSTOP) + + // ── Execution resumes here after "fg" ────────── + // Re-apply raw mode. + if termState != nil { + if s, err := term.MakeRaw(int(os.Stdin.Fd())); err == nil { + termState = s + } + } + // Continue the child (direct PID + process group). + syscall.Kill(childPid, syscall.SIGCONT) + syscall.Kill(-childPid, syscall.SIGCONT) + // Sync terminal size and ask copilot to redraw its UI. + pty.InheritSize(os.Stdin, ptmx) + syscall.Kill(childPid, syscall.SIGWINCH) + }() + i = j + 1 + } + } + if i < n { + ptmx.Write(buf[i:n]) + } + } + if err != nil { + return + } + } + }() + + // exitCodeCh receives the child's exit code when it terminates. + exitCodeCh := make(chan int, 1) + + // waitChild monitors the child for exit/signal; stop events are handled by + // the suspend goroutine above so we just loop past them here. + go func() { + for { + var ws syscall.WaitStatus + _, err := syscall.Wait4(childPid, &ws, syscall.WUNTRACED, nil) + if err != nil { + if err == syscall.EINTR { + continue + } + exitCodeCh <- 1 + return + } + + if ws.Exited() { + exitCodeCh <- ws.ExitStatus() + return + } + if ws.Signaled() { + exitCodeCh <- 128 + int(ws.Signal()) + return + } + // ws.Stopped() — suspend goroutine handles this; loop for next event. + } + }() if err := filterLoop(os.Stdout, ptmx, engine.ring); err != nil { log.Printf("filter: %v", err) } - if err := c.Wait(); err != nil { - var exitErr *exec.ExitError - if errors.As(err, &exitErr) { - return exitErr.ExitCode() - } - log.Printf("wait: %v", err) - return 1 - } - return 0 + return <-exitCodeCh }