exec.go

  1package tea
  2
  3import (
  4	"io"
  5	"os"
  6	"os/exec"
  7)
  8
  9// execMsg is used internally to run an ExecCommand sent with Exec.
 10type execMsg struct {
 11	cmd ExecCommand
 12	fn  ExecCallback
 13}
 14
 15// Exec is used to perform arbitrary I/O in a blocking fashion, effectively
 16// pausing the Program while execution is running and resuming it when
 17// execution has completed.
 18//
 19// Most of the time you'll want to use ExecProcess, which runs an exec.Cmd.
 20//
 21// For non-interactive i/o you should use a Cmd (that is, a tea.Cmd).
 22func Exec(c ExecCommand, fn ExecCallback) Cmd {
 23	return func() Msg {
 24		return execMsg{cmd: c, fn: fn}
 25	}
 26}
 27
 28// ExecProcess runs the given *exec.Cmd in a blocking fashion, effectively
 29// pausing the Program while the command is running. After the *exec.Cmd exists
 30// the Program resumes. It's useful for spawning other interactive applications
 31// such as editors and shells from within a Program.
 32//
 33// To produce the command, pass an *exec.Cmd and a function which returns
 34// a message containing the error which may have occurred when running the
 35// ExecCommand.
 36//
 37//	type VimFinishedMsg struct { err error }
 38//
 39//	c := exec.Command("vim", "file.txt")
 40//
 41//	cmd := ExecProcess(c, func(err error) Msg {
 42//	    return VimFinishedMsg{err: err}
 43//	})
 44//
 45// Or, if you don't care about errors, you could simply:
 46//
 47//	cmd := ExecProcess(exec.Command("vim", "file.txt"), nil)
 48//
 49// For non-interactive i/o you should use a Cmd (that is, a tea.Cmd).
 50func ExecProcess(c *exec.Cmd, fn ExecCallback) Cmd {
 51	return Exec(wrapExecCommand(c), fn)
 52}
 53
 54// ExecCallback is used when executing an *exec.Command to return a message
 55// with an error, which may or may not be nil.
 56type ExecCallback func(error) Msg
 57
 58// ExecCommand can be implemented to execute things in a blocking fashion in
 59// the current terminal.
 60type ExecCommand interface {
 61	Run() error
 62	SetStdin(io.Reader)
 63	SetStdout(io.Writer)
 64	SetStderr(io.Writer)
 65}
 66
 67// wrapExecCommand wraps an exec.Cmd so that it satisfies the ExecCommand
 68// interface so it can be used with Exec.
 69func wrapExecCommand(c *exec.Cmd) ExecCommand {
 70	return &osExecCommand{Cmd: c}
 71}
 72
 73// osExecCommand is a layer over an exec.Cmd that satisfies the ExecCommand
 74// interface.
 75type osExecCommand struct{ *exec.Cmd }
 76
 77// SetStdin sets stdin on underlying exec.Cmd to the given io.Reader.
 78func (c *osExecCommand) SetStdin(r io.Reader) {
 79	// If unset, have the command use the same input as the terminal.
 80	if c.Stdin == nil {
 81		c.Stdin = r
 82	}
 83}
 84
 85// SetStdout sets stdout on underlying exec.Cmd to the given io.Writer.
 86func (c *osExecCommand) SetStdout(w io.Writer) {
 87	// If unset, have the command use the same output as the terminal.
 88	if c.Stdout == nil {
 89		c.Stdout = w
 90	}
 91}
 92
 93// SetStderr sets stderr on the underlying exec.Cmd to the given io.Writer.
 94func (c *osExecCommand) SetStderr(w io.Writer) {
 95	// If unset, use stderr for the command's stderr
 96	if c.Stderr == nil {
 97		c.Stderr = w
 98	}
 99}
100
101// exec runs an ExecCommand and delivers the results to the program as a Msg.
102func (p *Program) exec(c ExecCommand, fn ExecCallback) {
103	if err := p.releaseTerminal(false); err != nil {
104		// If we can't release input, abort.
105		if fn != nil {
106			go p.Send(fn(err))
107		}
108		return
109	}
110
111	c.SetStdin(p.input)
112	c.SetStdout(p.output)
113	c.SetStderr(os.Stderr)
114
115	// Execute system command.
116	if err := c.Run(); err != nil {
117		p.renderer.resetLinesRendered()
118		_ = p.RestoreTerminal() // also try to restore the terminal.
119		if fn != nil {
120			go p.Send(fn(err))
121		}
122		return
123	}
124
125	// Maintain the existing output from the command
126	p.renderer.resetLinesRendered()
127
128	// Have the program re-capture input.
129	err := p.RestoreTerminal()
130	if fn != nil {
131		go p.Send(fn(err))
132	}
133}