diff --git a/go.mod b/go.mod index 6cc6d4a..f3c10cc 100644 --- a/go.mod +++ b/go.mod @@ -15,13 +15,12 @@ require ( github.com/charmbracelet/lipgloss v0.13.1 github.com/charmbracelet/log v0.4.0 github.com/charmbracelet/x/ansi v0.4.0 - github.com/charmbracelet/x/exp/term v0.0.0-20240403043919-dea9035a27d4 - github.com/creack/pty v1.1.23 + github.com/charmbracelet/x/term v0.2.1 + github.com/charmbracelet/x/xpty v0.1.1 github.com/kanrichan/resvg-go v0.0.2-0.20231001163256-63db194ca9f5 github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-runewidth v0.0.16 github.com/muesli/reflow v0.3.0 - golang.org/x/sys v0.28.0 ) require ( @@ -30,9 +29,11 @@ require ( github.com/catppuccin/go v0.2.0 // indirect github.com/charmbracelet/bubbles v0.20.0 // indirect github.com/charmbracelet/bubbletea v1.1.2 // indirect + github.com/charmbracelet/x/conpty v0.1.0 // indirect github.com/charmbracelet/x/errors v0.0.0-20240906161213-162f3037fef5 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240906161213-162f3037fef5 // indirect - github.com/charmbracelet/x/term v0.2.0 // indirect + github.com/charmbracelet/x/termios v0.1.0 // indirect + github.com/creack/pty v1.1.24 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect @@ -47,5 +48,6 @@ require ( github.com/tetratelabs/wazero v1.8.0 // indirect golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.18.0 // indirect ) diff --git a/go.sum b/go.sum index 3c1ef95..fb09b7b 100644 --- a/go.sum +++ b/go.sum @@ -34,16 +34,20 @@ github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8 github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= github.com/charmbracelet/x/ansi v0.4.0 h1:NqwHA4B23VwsDn4H3VcNX1W1tOmgnvY1NDx5tOXdnOU= github.com/charmbracelet/x/ansi v0.4.0/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= +github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= github.com/charmbracelet/x/errors v0.0.0-20240906161213-162f3037fef5 h1:rIt3LGU1yOC7U48eZjaAtjdzuSjH6Y0GA1KsRN7wqn8= github.com/charmbracelet/x/errors v0.0.0-20240906161213-162f3037fef5/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= github.com/charmbracelet/x/exp/strings v0.0.0-20240906161213-162f3037fef5 h1:73C9VsX8PMlXxVMKjg7ix67cZWg+zySdyzWRaXS239A= github.com/charmbracelet/x/exp/strings v0.0.0-20240906161213-162f3037fef5/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= -github.com/charmbracelet/x/exp/term v0.0.0-20240403043919-dea9035a27d4 h1:LewLBFkff+bCxgMZn1m8xNYQbUksWaY71d1QARHA11s= -github.com/charmbracelet/x/exp/term v0.0.0-20240403043919-dea9035a27d4/go.mod h1:6GZ13FjIP6eOCqWU4lqgveGnYxQo9c3qBzHPeFu4HBE= -github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= -github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= -github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= -github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/termios v0.1.0 h1:y4rjAHeFksBAfGbkRDmVinMg7x7DELIGAFbdNvxg97k= +github.com/charmbracelet/x/termios v0.1.0/go.mod h1:H/EVv/KRnrYjz+fCYa9bsKdqF3S8ouDK0AZEbG7r+/U= +github.com/charmbracelet/x/xpty v0.1.1 h1:A3DxWhvNjSkBR8/thRTcVnJ5bdv7OXRmChdMKtsi/5M= +github.com/charmbracelet/x/xpty v0.1.1/go.mod h1:jnu0EnSUYO5l+BExMLFiKVgpGPPV/RT2+6LYyCyf8zs= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= diff --git a/pty.go b/pty.go index 27e7f7c..d7e8271 100644 --- a/pty.go +++ b/pty.go @@ -1,46 +1,44 @@ -//go:build !windows -// +build !windows - package main import ( "bytes" "context" + "fmt" "io" "os" "os/exec" - "syscall" "github.com/caarlos0/go-shellwords" - "github.com/creack/pty" + "github.com/charmbracelet/x/term" + "github.com/charmbracelet/x/xpty" ) -// runInPty opens a new pty and runs the given command in it. -// The returned file is the pty's file descriptor and must be closed by the -// caller. -func (cfg Config) runInPty(c *exec.Cmd) (*os.File, error) { - //nolint: wrapcheck - return pty.StartWithAttrs(c, &pty.Winsize{ - Cols: 80, - Rows: 10, - X: uint16(cfg.Width), - }, &syscall.SysProcAttr{}) -} - func executeCommand(config Config) (string, error) { args, err := shellwords.Parse(config.Execute) if err != nil { - return "", err //nolint: wrapcheck + return "", fmt.Errorf("could not execute: %w", err) } + ctx, cancel := context.WithTimeout(context.Background(), config.ExecuteTimeout) defer cancel() - cmd := exec.CommandContext(ctx, args[0], args[1:]...) //nolint: gosec - pty, err := config.runInPty(cmd) + width, height, err := term.GetSize(os.Stdout.Fd()) if err != nil { - return "", err + width = 80 + height = 24 } - defer pty.Close() //nolint: errcheck + + pty, err := xpty.NewPty(width, height) + if err != nil { + return "", fmt.Errorf("could not execute: %w", err) + } + defer func() { _ = pty.Close() }() + + cmd := exec.CommandContext(ctx, args[0], args[1:]...) //nolint: gosec + if err := pty.Start(cmd); err != nil { + return "", fmt.Errorf("could not execute: %w", err) + } + var out bytes.Buffer var errorOut bytes.Buffer go func() { @@ -48,9 +46,8 @@ func executeCommand(config Config) (string, error) { errorOut.Write(out.Bytes()) }() - err = cmd.Wait() - if err != nil { - return errorOut.String(), err //nolint: wrapcheck + if err := xpty.WaitProcess(ctx, cmd); err != nil { + return errorOut.String(), fmt.Errorf("could not execute: %w", err) } return out.String(), nil } diff --git a/pty_windows.go b/pty_windows.go deleted file mode 100644 index 1a7a0c0..0000000 --- a/pty_windows.go +++ /dev/null @@ -1,78 +0,0 @@ -//go:build windows -// +build windows - -package main - -import ( - "bytes" - "context" - "io" - "os" - "syscall" - - "github.com/caarlos0/go-shellwords" - "github.com/charmbracelet/log" - "github.com/charmbracelet/x/exp/term/conpty" - "golang.org/x/sys/windows" -) - -func executeCommand(config Config) (string, error) { - args, err := shellwords.Parse(config.Execute) - if err != nil { - log.Error(err) - printErrorFatal("Something went wrong", err) - } - ctx, cancel := context.WithTimeout(context.Background(), config.ExecuteTimeout) - defer cancel() - - cpty, err := conpty.New(80, 10, 0) - if err != nil { - return "", err - } - defer cpty.Close() - - pid, proc, err := cpty.Spawn(args[0], args, &syscall.ProcAttr{Env: os.Environ()}) - if err != nil { - return "", err - } - - process, err := os.FindProcess(pid) - if err != nil { - // If we can't find the process via os.FindProcess, terminate the - // process as that's what we rely on for all further operations on the - // object. - if tErr := windows.TerminateProcess(windows.Handle(proc), 1); tErr != nil { - return "", tErr - } - return "", err - } - - type result struct { - *os.ProcessState - error - } - donec := make(chan result, 1) - go func() { - state, err := process.Wait() - donec <- result{state, err} - }() - - ctx, cancelFunc := context.WithTimeout(context.Background(), config.ExecuteTimeout) - defer cancelFunc() - var out bytes.Buffer - go func() { - _, _ = io.Copy(&out, cpty) - }() - - select { - case <-ctx.Done(): - err = windows.TerminateProcess(windows.Handle(proc), 1) - case r := <-donec: - err = r.error - } - - if err != nil { - return "", err - } - return out.String(), nil -}