diff --git a/go.mod b/go.mod index 4e170fd..2ad7734 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( 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.15 + golang.org/x/sys v0.18.0 ) require ( @@ -25,6 +26,7 @@ require ( github.com/catppuccin/go v0.2.0 // indirect github.com/charmbracelet/bubbles v0.18.0 // indirect github.com/charmbracelet/bubbletea v0.25.0 // indirect + github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240327191656-1e1cd98f30d4 // indirect github.com/containerd/console v1.0.4 // indirect github.com/dlclark/regexp2 v1.11.0 // indirect @@ -40,7 +42,6 @@ require ( github.com/tetratelabs/wazero v1.7.0 // indirect golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect golang.org/x/sync v0.6.0 // indirect - golang.org/x/sys v0.18.0 // indirect golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index c9de030..1755885 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,8 @@ github.com/charmbracelet/lipgloss v0.10.1-0.20240325130315-f16ea2bdcb88 h1:mgadl github.com/charmbracelet/lipgloss v0.10.1-0.20240325130315-f16ea2bdcb88/go.mod h1:7V+J+a41Lz/77Nsya9Srb7WFRj02LvdMcAXZtc4zroQ= github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= +github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651 h1:3RXpZWGWTOeVXCTv0Dnzxdv/MhNUkBfEcbaTY0zrTQI= +github.com/charmbracelet/x/errors v0.0.0-20240117030013-d31dba354651/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= github.com/charmbracelet/x/exp/strings v0.0.0-20240327191656-1e1cd98f30d4 h1:E3qdydPjQJIGnT6t6lY0P70sou+Vmh+VcQHAYR4qTtA= github.com/charmbracelet/x/exp/strings v0.0.0-20240327191656-1e1cd98f30d4/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= github.com/charmbracelet/x/exp/term v0.0.0-20240327191656-1e1cd98f30d4 h1:WckoQq/mT+RkBq1JaCWNp65snXleZzQEEu/c3AL0TTU= diff --git a/main.go b/main.go index dff58c4..53a3f25 100644 --- a/main.go +++ b/main.go @@ -2,12 +2,9 @@ package main import ( "bytes" - "context" "errors" "fmt" - "io" "os" - "os/exec" "path/filepath" "strings" @@ -17,7 +14,6 @@ import ( "github.com/alecthomas/chroma/v2/styles" "github.com/alecthomas/kong" "github.com/beevik/etree" - "github.com/caarlos0/go-shellwords" in "github.com/charmbracelet/freeze/input" "github.com/charmbracelet/freeze/svg" "github.com/charmbracelet/lipgloss" @@ -51,7 +47,13 @@ func main() { // Copy the pty output to buffer if config.Execute != "" { - input = executeCommand(config) + input, err = executeCommand(config) + if err != nil { + printErrorFatal("Something went wrong", err) + } + if input == "" { + printErrorFatal("Something went wrong", errors.New("no command output")) + } } isDefaultConfig := config.Config == "default" @@ -405,31 +407,6 @@ func main() { } } -func executeCommand(config Config) string { - args, err := shellwords.Parse(config.Execute) - if err != nil { - printErrorFatal("Something went wrong", err) - } - ctx, cancel := context.WithTimeout(context.Background(), config.ExecuteTimeout) - defer cancel() - - cmd := exec.CommandContext(ctx, args[0], args[1:]...) - pty, err := config.runInPty(cmd) - if err != nil { - printErrorFatal("Something went wrong", err) - } - defer pty.Close() - var out bytes.Buffer - go func() { - _, _ = io.Copy(&out, pty) - }() - err = cmd.Wait() - if err != nil { - printError("Command failed", err) - } - return out.String() -} - var outputHeader = lipgloss.NewStyle().Foreground(lipgloss.Color("#F1F1F1")).Background(lipgloss.Color("#6C50FF")).Bold(true).Padding(0, 1).MarginRight(1).SetString("WROTE") func printFilenameOutput(filename string) { diff --git a/pty.go b/pty.go index 84929a4..7c782b8 100644 --- a/pty.go +++ b/pty.go @@ -1,10 +1,17 @@ +//go:build !windows +// +build !windows + package main import ( + "bytes" + "context" + "io" "os" "os/exec" "syscall" + "github.com/caarlos0/go-shellwords" "github.com/creack/pty" ) @@ -18,3 +25,29 @@ func (cfg Config) runInPty(c *exec.Cmd) (*os.File, error) { X: uint16(cfg.Width), }, &syscall.SysProcAttr{}) } + +func executeCommand(config Config) (string, error) { + args, err := shellwords.Parse(config.Execute) + if err != nil { + return "", err + } + ctx, cancel := context.WithTimeout(context.Background(), config.ExecuteTimeout) + defer cancel() + + cmd := exec.CommandContext(ctx, args[0], args[1:]...) + pty, err := config.runInPty(cmd) + if err != nil { + return "", err + } + defer pty.Close() + var out bytes.Buffer + go func() { + _, _ = io.Copy(&out, pty) + }() + + err = cmd.Wait() + if err != nil { + return "", err + } + return out.String(), nil +} diff --git a/pty_windows.go b/pty_windows.go new file mode 100644 index 0000000..1a7a0c0 --- /dev/null +++ b/pty_windows.go @@ -0,0 +1,78 @@ +//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 +}