chore(exec): small API edits (#323)

* feat: add OSExec helper function for running exec.Cmds

* chore: for now, un-expose WrapExecCommand

* chore: move exec struff into its own file

* chore(exec): better name for Exec func that wraps exec.Cmd (thanks, @toby)
This commit is contained in:
Christian Rocha 2022-06-01 17:12:21 -07:00 committed by GitHub
parent 97050569c9
commit 775dbfbeff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 131 additions and 114 deletions

View File

@ -12,7 +12,7 @@ type editorFinishedMsg struct{ err error }
func openEditor() tea.Cmd { func openEditor() tea.Cmd {
c := exec.Command(os.Getenv("EDITOR")) //nolint:gosec c := exec.Command(os.Getenv("EDITOR")) //nolint:gosec
return tea.Exec(tea.WrapExecCommand(c), func(err error) tea.Msg { return tea.ExecProcess(c, func(err error) tea.Msg {
return editorFinishedMsg{err} return editorFinishedMsg{err}
}) })
} }

129
exec.go Normal file
View File

@ -0,0 +1,129 @@
package tea
import (
"io"
"os"
"os/exec"
)
// execMsg is used internally to run an ExecCommand sent with Exec.
type execMsg struct {
cmd ExecCommand
fn ExecCallback
}
// Exec is used to perform arbitrary I/O in a blocking fashion, effectively
// pausing the Program while execution is runnning and resuming it when
// execution has completed.
//
// Most of the time you'll want to use ExecProcess, which runs an exec.Cmd.
//
// For non-interactive i/o you should use a Cmd (that is, a tea.Cmd).
func Exec(c ExecCommand, fn ExecCallback) Cmd {
return func() Msg {
return execMsg{cmd: c, fn: fn}
}
}
// ExecProcess runs the given *exec.Cmd in a blocking fashion, effectively
// pausing the Program while the command is running. After the *exec.Cmd exists
// the Program resumes. It's useful for spawning other interactive applications
// such as editors and shells from within a Program.
//
// To produce the command, pass an *exec.Cmd and a function which returns
// a message containing the error which may have occurred when running the
// ExecCommand.
//
// type VimFinishedMsg struct { err error }
//
// c := exec.Command("vim", "file.txt")
//
// cmd := ExecProcess(c, func(err error) Msg {
// return VimFinishedMsg{err: error}
// })
//
// Or, if you don't care about errors, you could simply:
//
// cmd := ExecProcess(exec.Command("vim", "file.txt"), nil)
//
// For non-interactive i/o you should use a Cmd (that is, a tea.Cmd).
func ExecProcess(c *exec.Cmd, fn ExecCallback) Cmd {
return Exec(wrapExecCommand(c), fn)
}
// ExecCallback is used when executing an *exec.Command to return a message
// with an error, which may or may not be nil.
type ExecCallback func(error) Msg
// ExecCommand can be implemented to execute things in a blocking fashion in
// the current terminal.
type ExecCommand interface {
Run() error
SetStdin(io.Reader)
SetStdout(io.Writer)
SetStderr(io.Writer)
}
// wrapExecCommand wraps an exec.Cmd so that it satisfies the ExecCommand
// interface so it can be used with Exec.
func wrapExecCommand(c *exec.Cmd) ExecCommand {
return &osExecCommand{Cmd: c}
}
// osExecCommand is a layer over an exec.Cmd that satisfies the ExecCommand
// interface.
type osExecCommand struct{ *exec.Cmd }
// SetStdin sets stdin on underlying exec.Cmd to the given io.Reader.
func (c *osExecCommand) SetStdin(r io.Reader) {
// If unset, have the command use the same input as the terminal.
if c.Stdin == nil {
c.Stdin = r
}
}
// SetStdout sets stdout on underlying exec.Cmd to the given io.Writer.
func (c *osExecCommand) SetStdout(w io.Writer) {
// If unset, have the command use the same output as the terminal.
if c.Stdout == nil {
c.Stdout = w
}
}
// SetStderr sets stderr on the underlying exec.Cmd to the given io.Writer.
func (c *osExecCommand) SetStderr(w io.Writer) {
// If unset, use stderr for the command's stderr
if c.Stderr == nil {
c.Stderr = w
}
}
// exec runs an ExecCommand and delivers the results to the program as a Msg.
func (p *Program) exec(c ExecCommand, fn ExecCallback) {
if err := p.ReleaseTerminal(); err != nil {
// If we can't release input, abort.
if fn != nil {
go p.Send(fn(err))
}
return
}
c.SetStdin(p.input)
c.SetStdout(p.output)
c.SetStderr(os.Stderr)
// Execute system command.
if err := c.Run(); err != nil {
_ = p.RestoreTerminal() // also try to restore the terminal.
if fn != nil {
go p.Send(fn(err))
}
return
}
// Have the program re-capture input.
err := p.RestoreTerminal()
if fn != nil {
go p.Send(fn(err))
}
}

114
tea.go
View File

@ -14,7 +14,6 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"os/exec"
"os/signal" "os/signal"
"runtime/debug" "runtime/debug"
"sync" "sync"
@ -246,44 +245,6 @@ func HideCursor() Msg {
return hideCursorMsg{} return hideCursorMsg{}
} }
// Exec runs the given ExecCommand in a blocking fashion, effectively pausing
// the Program while the command is running. After the *exec.Cmd exists the
// Program resumes. It's useful for spawning other interactive applications
// such as editors and shells from within a Program.
//
// To produce the command, pass an ExecCommand and a function which returns
// a message containing the error which may have occurred when running the
// ExecCommand.
//
// type VimFinishedMsg struct { err error }
//
// c := exec.Command("vim", "file.txt")
//
// cmd := Exec(WrapExecCommand(c), func(err error) Msg {
// return VimFinishedMsg{err: error}
// })
//
// Or, if you don't care about errors you could simply:
//
// cmd := Exec(WrapExecCommand(exec.Command("vim", "file.txt")), nil)
//
// For non-interactive i/o you should use a Cmd (that is, a tea.Cmd).
func Exec(c ExecCommand, fn ExecCallback) Cmd {
return func() Msg {
return execMsg{cmd: c, fn: fn}
}
}
// ExecCallback is used when executing an *exec.Command to return a message
// with an error, which may or may not be nil.
type ExecCallback func(error) Msg
// execMsg is used internally to run an ExecCommand sent with Exec.
type execMsg struct {
cmd ExecCommand
fn ExecCallback
}
// hideCursorMsg is an internal command used to hide the cursor. You can send // hideCursorMsg is an internal command used to hide the cursor. You can send
// this message with HideCursor. // this message with HideCursor.
type hideCursorMsg struct{} type hideCursorMsg struct{}
@ -564,7 +525,7 @@ func (p *Program) StartReturningModel() (Model, error) {
hideCursor(p.output) hideCursor(p.output)
case execMsg: case execMsg:
// Note: this blocks. // NB: this blocks.
p.exec(msg.cmd, msg.fn) p.exec(msg.cmd, msg.fn)
} }
@ -746,76 +707,3 @@ func (p *Program) RestoreTerminal() error {
return nil return nil
} }
// ExecCommand can be implemented to execute things in the current
// terminal using the Exec Cmd.
type ExecCommand interface {
Run() error
SetStdin(io.Reader)
SetStdout(io.Writer)
SetStderr(io.Writer)
}
// WrapExecCommand wraps an exec.Cmd so that it satisfies the ExecCommand
// interface.
func WrapExecCommand(c *exec.Cmd) ExecCommand {
return &osExecCommand{Cmd: c}
}
// osExecCommand is a layer over an exec.Cmd that satisfies the ExecCommand
// interface.
type osExecCommand struct{ *exec.Cmd }
// SetStdin sets stdin on underlying exec.Cmd to the given io.Reader.
func (c *osExecCommand) SetStdin(r io.Reader) {
// If unset, have the command use the same input as the terminal.
if c.Stdin == nil {
c.Stdin = r
}
}
// SetStdout sets stdout on underlying exec.Cmd to the given io.Writer.
func (c *osExecCommand) SetStdout(w io.Writer) {
// If unset, have the command use the same output as the terminal.
if c.Stdout == nil {
c.Stdout = w
}
}
// SetStderr sets stderr on the underlying exec.Cmd to the given io.Writer.
func (c *osExecCommand) SetStderr(w io.Writer) {
// If unset, use stderr for the command's stderr
if c.Stderr == nil {
c.Stderr = w
}
}
// exec runs an ExecCommand and delivers the results to the program as a Msg.
func (p *Program) exec(c ExecCommand, fn ExecCallback) {
if err := p.ReleaseTerminal(); err != nil {
// If we can't release input, abort.
if fn != nil {
go p.Send(fn(err))
}
return
}
c.SetStdin(p.input)
c.SetStdout(p.output)
c.SetStderr(os.Stderr)
// Execute system command.
if err := c.Run(); err != nil {
_ = p.RestoreTerminal() // also try to restore the terminal.
if fn != nil {
go p.Send(fn(err))
}
return
}
// Have the program re-capture input.
err := p.RestoreTerminal()
if fn != nil {
go p.Send(fn(err))
}
}