Changed something

This commit is contained in:
2026-04-23 08:44:13 +02:00
parent 7ff7fdd0cc
commit 65d87acb53

115
main.go
View File

@@ -460,8 +460,9 @@ func run(name string, args []string) int {
}() }()
// Put stdin in raw mode if it is a terminal. // Put stdin in raw mode if it is a terminal.
var oldState *term.State
if term.IsTerminal(int(os.Stdin.Fd())) { 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 { if err != nil {
log.Printf("make raw: %v", err) log.Printf("make raw: %v", err)
return 1 return 1
@@ -469,19 +470,111 @@ func run(name string, args []string) int {
defer term.Restore(int(os.Stdin.Fd()), oldState) 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 { if err := filterLoop(os.Stdout, ptmx, engine.ring); err != nil {
log.Printf("filter: %v", err) log.Printf("filter: %v", err)
} }
if err := c.Wait(); err != nil { return <-exitCodeCh
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
return exitErr.ExitCode()
}
log.Printf("wait: %v", err)
return 1
}
return 0
} }