95 lines
2.2 KiB
Go
95 lines
2.2 KiB
Go
//go:build !windows
|
|
|
|
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
|
|
"github.com/creack/pty"
|
|
"golang.org/x/term"
|
|
|
|
"os/exec"
|
|
)
|
|
|
|
// runWithPTY starts cmd attached to a new pseudo-terminal and relays I/O
|
|
// between the outer terminal and the PTY master. This lets TUI apps render
|
|
// correctly while bifrost retains control of the process.
|
|
func runWithPTY(ctx context.Context, stdout io.Writer, cmd *exec.Cmd) error {
|
|
// Get the initial terminal size from the real terminal
|
|
sz, err := pty.GetsizeFull(os.Stdin)
|
|
if err != nil {
|
|
// Fallback to a reasonable default if stdin isn't a terminal
|
|
sz = &pty.Winsize{Rows: 24, Cols: 80}
|
|
}
|
|
|
|
// Start the command with a PTY attached, sized to match the outer terminal
|
|
ptmx, err := pty.StartWithSize(cmd, sz)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer ptmx.Close()
|
|
|
|
// Handle SIGWINCH — propagate terminal resizes to the PTY
|
|
sigCh := make(chan os.Signal, 1)
|
|
signal.Notify(sigCh, syscall.SIGWINCH)
|
|
done := make(chan struct{})
|
|
defer func() {
|
|
signal.Stop(sigCh)
|
|
close(done)
|
|
}()
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-sigCh:
|
|
if newSz, err := pty.GetsizeFull(os.Stdin); err == nil {
|
|
_ = pty.Setsize(ptmx, newSz)
|
|
}
|
|
case <-done:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Put the outer terminal into raw mode so keystrokes (Ctrl-C, etc.)
|
|
// are forwarded as bytes to the PTY rather than handled by the OS.
|
|
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
|
|
if err != nil {
|
|
// If we can't go raw (e.g., piped input), continue without it
|
|
oldState = nil
|
|
}
|
|
if oldState != nil {
|
|
defer func() {
|
|
_ = term.Restore(int(os.Stdin.Fd()), oldState)
|
|
_, _ = io.WriteString(stdout, hostCursorResetSequence())
|
|
}()
|
|
}
|
|
|
|
// Relay stdout: PTY master → caller's stdout
|
|
// This goroutine exits when the child process dies and the PTY master
|
|
// returns EOF.
|
|
outDone := make(chan struct{})
|
|
go func() {
|
|
defer close(outDone)
|
|
_, _ = io.Copy(stdout, ptmx)
|
|
}()
|
|
|
|
// Relay stdin: outer terminal → PTY master
|
|
// This goroutine will block on os.Stdin.Read after the child exits;
|
|
// that's expected and harmless — it unblocks on the next keystroke.
|
|
go func() {
|
|
_, _ = io.Copy(ptmx, os.Stdin)
|
|
}()
|
|
|
|
// Wait for the command to finish
|
|
err = cmd.Wait()
|
|
|
|
// Drain any remaining PTY output
|
|
<-outDone
|
|
|
|
return err
|
|
}
|