Initial patterm project
This commit is contained in:
142
internal/pty/pty.go
Normal file
142
internal/pty/pty.go
Normal file
@@ -0,0 +1,142 @@
|
||||
// Package pty wraps creack/pty with the small surface the spike needs.
|
||||
package pty
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
cpty "github.com/creack/pty"
|
||||
)
|
||||
|
||||
// PTY holds a child process attached to a pseudo-terminal master fd.
|
||||
type PTY struct {
|
||||
master *os.File
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
// Start spawns argv with stdin/stdout/stderr attached to a new PTY sized
|
||||
// (cols, rows). The returned PTY exposes the master fd for the parent to
|
||||
// read from and write to.
|
||||
func Start(argv []string, env []string, cols, rows uint16) (*PTY, error) {
|
||||
if len(argv) == 0 {
|
||||
return nil, fmt.Errorf("pty: empty argv")
|
||||
}
|
||||
cmd := exec.Command(argv[0], argv[1:]...)
|
||||
if env != nil {
|
||||
cmd.Env = ensureTerm(env)
|
||||
} else {
|
||||
// Default to the parent environment but force TERM to xterm-256color
|
||||
// so child programs assume something modern and we observe SGR + alt
|
||||
// screen sequences.
|
||||
cmd.Env = ensureTerm(os.Environ())
|
||||
}
|
||||
|
||||
ws := &cpty.Winsize{Cols: cols, Rows: rows}
|
||||
master, err := cpty.StartWithSize(cmd, ws)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pty: start %v: %w", argv, err)
|
||||
}
|
||||
return &PTY{master: master, cmd: cmd}, nil
|
||||
}
|
||||
|
||||
func (p *PTY) Read(b []byte) (int, error) {
|
||||
if p.master == nil {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
return p.master.Read(b)
|
||||
}
|
||||
|
||||
func (p *PTY) Write(b []byte) (int, error) {
|
||||
if p.master == nil {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
return p.master.Write(b)
|
||||
}
|
||||
|
||||
func (p *PTY) Resize(cols, rows uint16) error {
|
||||
if p.master == nil {
|
||||
return io.ErrClosedPipe
|
||||
}
|
||||
return cpty.Setsize(p.master, &cpty.Winsize{Cols: cols, Rows: rows})
|
||||
}
|
||||
|
||||
// Wait blocks until the child exits and returns its exit error if any.
|
||||
func (p *PTY) Wait() error {
|
||||
if p.cmd == nil {
|
||||
return nil
|
||||
}
|
||||
return p.cmd.Wait()
|
||||
}
|
||||
|
||||
// Pid returns the child's PID, or -1 if the process is not running.
|
||||
func (p *PTY) Pid() int {
|
||||
if p.cmd == nil || p.cmd.Process == nil {
|
||||
return -1
|
||||
}
|
||||
return p.cmd.Process.Pid
|
||||
}
|
||||
|
||||
// Close terminates the child (best effort) and releases the master fd.
|
||||
func (p *PTY) Close() error {
|
||||
var firstErr error
|
||||
if p.master != nil {
|
||||
if err := p.master.Close(); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
p.master = nil
|
||||
}
|
||||
if p.cmd != nil && p.cmd.Process != nil {
|
||||
_ = p.cmd.Process.Kill()
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
|
||||
// envDefaults are added to the child's environment unless the parent already
|
||||
// set them. Modern agent TUIs check these and silently downgrade rendering
|
||||
// when they're missing (no truecolor, ASCII-only banners, etc.).
|
||||
var envDefaults = map[string]string{
|
||||
"TERM": "xterm-256color",
|
||||
"COLORTERM": "truecolor",
|
||||
}
|
||||
|
||||
// envStrip names variables we DROP before launching a child. COLUMNS /
|
||||
// LINES inherited from the parent shell describe the *host* terminal,
|
||||
// not the PTY we created — when they leak through, TUIs that prefer
|
||||
// env over TIOCGWINSZ render past the PTY's actual cell grid and
|
||||
// overwrite our chrome.
|
||||
var envStrip = map[string]bool{
|
||||
"COLUMNS": true,
|
||||
"LINES": true,
|
||||
}
|
||||
|
||||
func ensureTerm(env []string) []string {
|
||||
have := make(map[string]bool, len(envDefaults))
|
||||
out := make([]string, 0, len(env)+len(envDefaults))
|
||||
for _, kv := range env {
|
||||
key := envKey(kv)
|
||||
if envStrip[key] {
|
||||
continue
|
||||
}
|
||||
if _, isDefault := envDefaults[key]; isDefault {
|
||||
have[key] = true
|
||||
}
|
||||
out = append(out, kv)
|
||||
}
|
||||
for k, v := range envDefaults {
|
||||
if !have[k] {
|
||||
out = append(out, k+"="+v)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func envKey(kv string) string {
|
||||
for i := 0; i < len(kv); i++ {
|
||||
if kv[i] == '=' {
|
||||
return kv[:i]
|
||||
}
|
||||
}
|
||||
return kv
|
||||
}
|
||||
Reference in New Issue
Block a user