Skip to content

Commit

Permalink
feat: add wish.Command and wish.Cmd
Browse files Browse the repository at this point in the history
The wish-exec example with vim worked because neovim was not using
STDERR.

Bubbletea doesn't have a concept of stdout and stderr, just output, so
`tea.ExecProcess` sets the `exec.Cmd` stderr to `os.Stderr`.

This would fail for bash, for instance.

This also introduces a `wish.Cmd` type and a `wish.Command`
function to properly set up a `wish.Cmd` based on `ssh.Session` (and
optionally a Pty), which can then be used with `tea.Exec`.

Finally, it adds to the wish-exec example, including the `s` key to run
a shell (bash).

closes #228

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
  • Loading branch information
caarlos0 committed Jan 22, 2024
1 parent e5d20f5 commit 6b83fbb
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 8 deletions.
24 changes: 16 additions & 8 deletions examples/wish-exec/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
Expand Down Expand Up @@ -77,26 +76,35 @@ func (m model) Init() tea.Cmd {
return nil
}

type vimFinishedMsg struct{ err error }
type cmdFinishedMsg struct{ err error }

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
// PS: the execs won't work on windows.
switch msg.String() {
case "e":
// PS: this does not work on Windows.
c := exec.Command("vim", "file.txt")
cmd := tea.ExecProcess(c, func(err error) tea.Msg {
c := wish.Command(m.sess, "vim", "file.txt")
cmd := tea.Exec(c, func(err error) tea.Msg {
if err != nil {
log.Error("vim finished", "error", err)
}
return vimFinishedMsg{err: err}
return cmdFinishedMsg{err: err}
})
return m, cmd
case "s":
c := wish.Command(m.sess, "bash", "-im")
cmd := tea.Exec(c, func(err error) tea.Msg {
if err != nil {
log.Error("shell finished", "error", err)
}
return cmdFinishedMsg{err: err}
})
return m, cmd
case "q", "ctrl+c":
return m, tea.Quit
}
case vimFinishedMsg:
case cmdFinishedMsg:
m.err = msg.err
return m, nil
}
Expand All @@ -108,5 +116,5 @@ func (m model) View() string {
if m.err != nil {
return m.errStyle.Render(m.err.Error() + "\n")
}
return m.style.Render("Press 'e' to edit or 'q' to quit...\n")
return m.style.Render("Press 'e' to edit, 's' to hop into a shell, or 'q' to quit...\n")
}
62 changes: 62 additions & 0 deletions wish.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package wish
import (
"fmt"
"io"
"os/exec"
"runtime"
"time"

"github.com/charmbracelet/keygen"
"github.com/charmbracelet/ssh"
Expand Down Expand Up @@ -98,3 +101,62 @@ func Println(s ssh.Session, v ...interface{}) {
func WriteString(s ssh.Session, v string) (int, error) {
return io.WriteString(s, v)
}

// Command sets stdin, stdout, and stderr to the current session's PTY slave.
//
// If the current session does not have a PTY, it sets them to the session
// itself.
func Command(s ssh.Session, name string, args ...string) *Cmd {
c := exec.Command(name, args...)
pty, _, ok := s.Pty()
if !ok {
c.Stdin, c.Stdout, c.Stderr = s, s, s
return &Cmd{cmd: c}
}

Check warning on line 115 in wish.go

View check run for this annotation

Codecov / codecov/patch

wish.go#L109-L115

Added lines #L109 - L115 were not covered by tests

return &Cmd{c, &pty}

Check warning on line 117 in wish.go

View check run for this annotation

Codecov / codecov/patch

wish.go#L117

Added line #L117 was not covered by tests
}

// Cmd wraps a *exec.Cmd and a ssh.Pty so a command can be properly run.
type Cmd struct {
cmd *exec.Cmd
pty *ssh.Pty
}

// SetStderr conforms with tea.ExecCommand.
func (*Cmd) SetStderr(io.Writer) {}

Check warning on line 127 in wish.go

View check run for this annotation

Codecov / codecov/patch

wish.go#L127

Added line #L127 was not covered by tests

// SetStdin conforms with tea.ExecCommand.
func (*Cmd) SetStdin(io.Reader) {}

Check warning on line 130 in wish.go

View check run for this annotation

Codecov / codecov/patch

wish.go#L130

Added line #L130 was not covered by tests

// SetStdout conforms with tea.ExecCommand.
func (*Cmd) SetStdout(io.Writer) {}

Check warning on line 133 in wish.go

View check run for this annotation

Codecov / codecov/patch

wish.go#L133

Added line #L133 was not covered by tests

// Run runs the program and waits for it to finish.
func (c *Cmd) Run() error {
if c.pty == nil {
return c.Run()
}

Check warning on line 139 in wish.go

View check run for this annotation

Codecov / codecov/patch

wish.go#L136-L139

Added lines #L136 - L139 were not covered by tests

if err := c.pty.Start(c.cmd); err != nil {
return err
}
start := time.Now()
if runtime.GOOS == "windows" {
for c.cmd.ProcessState == nil {
if time.Since(start) > time.Second*10 {
return fmt.Errorf("could not start process")
}
time.Sleep(100 * time.Millisecond)

Check warning on line 150 in wish.go

View check run for this annotation

Codecov / codecov/patch

wish.go#L141-L150

Added lines #L141 - L150 were not covered by tests
}

if !c.cmd.ProcessState.Success() {
return fmt.Errorf("process failed: exit %d", c.cmd.ProcessState.ExitCode())
}
} else {
if err := c.cmd.Wait(); err != nil {
return err
}

Check warning on line 159 in wish.go

View check run for this annotation

Codecov / codecov/patch

wish.go#L153-L159

Added lines #L153 - L159 were not covered by tests
}
return nil

Check warning on line 161 in wish.go

View check run for this annotation

Codecov / codecov/patch

wish.go#L161

Added line #L161 was not covered by tests
}

0 comments on commit 6b83fbb

Please sign in to comment.