From 6b83fbb0a892e1d78d4b05de2ac0a71cb1d18c16 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 22 Jan 2024 09:28:16 -0300 Subject: [PATCH 01/13] feat: add wish.Command and wish.Cmd 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 --- examples/wish-exec/main.go | 24 ++++++++++----- wish.go | 62 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/examples/wish-exec/main.go b/examples/wish-exec/main.go index 11377796..ad94ffcf 100644 --- a/examples/wish-exec/main.go +++ b/examples/wish-exec/main.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "os" - "os/exec" "os/signal" "syscall" "time" @@ -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 } @@ -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") } diff --git a/wish.go b/wish.go index dbfc8f24..cece0c44 100644 --- a/wish.go +++ b/wish.go @@ -3,6 +3,9 @@ package wish import ( "fmt" "io" + "os/exec" + "runtime" + "time" "github.com/charmbracelet/keygen" "github.com/charmbracelet/ssh" @@ -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} + } + + return &Cmd{c, &pty} +} + +// 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) {} + +// SetStdin conforms with tea.ExecCommand. +func (*Cmd) SetStdin(io.Reader) {} + +// SetStdout conforms with tea.ExecCommand. +func (*Cmd) SetStdout(io.Writer) {} + +// Run runs the program and waits for it to finish. +func (c *Cmd) Run() error { + if c.pty == nil { + return c.Run() + } + + 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) + } + + 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 + } + } + return nil +} From b76e30aff8f0cf7a619936decfd543ccc0fa6dc6 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 22 Jan 2024 10:22:08 -0300 Subject: [PATCH 02/13] feat: allow to set dir and env --- wish.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/wish.go b/wish.go index cece0c44..2a221712 100644 --- a/wish.go +++ b/wish.go @@ -123,6 +123,21 @@ type Cmd struct { pty *ssh.Pty } +// SetDir set the underlying exec.Cmd env. +func (c *Cmd) SetEnv(env []string) { + c.cmd.Env = env +} + +// Environ returns the underlying exec.Cmd environment. +func (c *Cmd) Environ() []string { + return c.cmd.Environ() +} + +// SetDir set the underlying exec.Cmd dir. +func (c *Cmd) SetDir(dir string) { + c.cmd.Dir = dir +} + // SetStderr conforms with tea.ExecCommand. func (*Cmd) SetStderr(io.Writer) {} From 15c8f2f1bf3c69c58fc2256ea9bdbed8632a95fb Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 22 Jan 2024 10:23:48 -0300 Subject: [PATCH 03/13] refactory: move things around a bit Signed-off-by: Carlos Alexandro Becker --- cmd.go | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ wish.go | 77 --------------------------------------------------- 2 files changed, 85 insertions(+), 77 deletions(-) create mode 100644 cmd.go diff --git a/cmd.go b/cmd.go new file mode 100644 index 00000000..7bcc67ed --- /dev/null +++ b/cmd.go @@ -0,0 +1,85 @@ +package wish + +import ( + "fmt" + "io" + "os/exec" + "runtime" + "time" + + "github.com/charmbracelet/ssh" +) + +// 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} + } + + return &Cmd{c, &pty} +} + +// 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 +} + +// SetDir set the underlying exec.Cmd env. +func (c *Cmd) SetEnv(env []string) { + c.cmd.Env = env +} + +// Environ returns the underlying exec.Cmd environment. +func (c *Cmd) Environ() []string { + return c.cmd.Environ() +} + +// SetDir set the underlying exec.Cmd dir. +func (c *Cmd) SetDir(dir string) { + c.cmd.Dir = dir +} + +// SetStderr conforms with tea.ExecCommand. +func (*Cmd) SetStderr(io.Writer) {} + +// SetStdin conforms with tea.ExecCommand. +func (*Cmd) SetStdin(io.Reader) {} + +// SetStdout conforms with tea.ExecCommand. +func (*Cmd) SetStdout(io.Writer) {} + +// Run runs the program and waits for it to finish. +func (c *Cmd) Run() error { + if c.pty == nil { + return c.Run() + } + + 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) + } + + 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 + } + } + return nil +} diff --git a/wish.go b/wish.go index 2a221712..dbfc8f24 100644 --- a/wish.go +++ b/wish.go @@ -3,9 +3,6 @@ package wish import ( "fmt" "io" - "os/exec" - "runtime" - "time" "github.com/charmbracelet/keygen" "github.com/charmbracelet/ssh" @@ -101,77 +98,3 @@ 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} - } - - return &Cmd{c, &pty} -} - -// 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 -} - -// SetDir set the underlying exec.Cmd env. -func (c *Cmd) SetEnv(env []string) { - c.cmd.Env = env -} - -// Environ returns the underlying exec.Cmd environment. -func (c *Cmd) Environ() []string { - return c.cmd.Environ() -} - -// SetDir set the underlying exec.Cmd dir. -func (c *Cmd) SetDir(dir string) { - c.cmd.Dir = dir -} - -// SetStderr conforms with tea.ExecCommand. -func (*Cmd) SetStderr(io.Writer) {} - -// SetStdin conforms with tea.ExecCommand. -func (*Cmd) SetStdin(io.Reader) {} - -// SetStdout conforms with tea.ExecCommand. -func (*Cmd) SetStdout(io.Writer) {} - -// Run runs the program and waits for it to finish. -func (c *Cmd) Run() error { - if c.pty == nil { - return c.Run() - } - - 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) - } - - 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 - } - } - return nil -} From 5b50e2e9c5bc153496d37ac718622b35cc176674 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 22 Jan 2024 11:46:42 -0300 Subject: [PATCH 04/13] fix: loop --- cmd.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd.go b/cmd.go index 7bcc67ed..6f1aec85 100644 --- a/cmd.go +++ b/cmd.go @@ -58,7 +58,7 @@ func (*Cmd) SetStdout(io.Writer) {} // Run runs the program and waits for it to finish. func (c *Cmd) Run() error { if c.pty == nil { - return c.Run() + return c.cmd.Run() } if err := c.pty.Start(c.cmd); err != nil { From f8bb3d18e4046bb630d17960835fa74157f208b5 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 22 Jan 2024 11:55:33 -0300 Subject: [PATCH 05/13] test: add some tests Signed-off-by: Carlos Alexandro Becker --- cmd_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 cmd_test.go diff --git a/cmd_test.go b/cmd_test.go new file mode 100644 index 00000000..5cc35e07 --- /dev/null +++ b/cmd_test.go @@ -0,0 +1,46 @@ +package wish + +import ( + "bytes" + "strings" + "testing" + + "github.com/charmbracelet/ssh" + "github.com/charmbracelet/wish/testsession" +) + +func TestCommandNoPty(t *testing.T) { + tmp := t.TempDir() + sess := testsession.New(t, &ssh.Server{ + Handler: func(s ssh.Session) { + if err := Command(s, "echo", "hello").Run(); err != nil { + Fatal(s, "echo:", err) + } + + cmd := Command(s, "env") + cmd.SetEnv([]string{"HELLO=hello"}) + if err := cmd.Run(); err != nil { + Fatal(s, "env:", err) + } + + cmd = Command(s, "pwd") + cmd.SetDir(tmp) + if err := cmd.Run(); err != nil { + Fatal(s, "pwd:", err) + } + }, + }, nil) + var out bytes.Buffer + sess.Stdout = &out + if err := sess.Run(""); err != nil { + t.Errorf("expected no error, got %v", err) + } + expect := strings.Join([]string{ + "hello", + "HELLO=hello", + tmp, + }, "\n") + "\n" + if s := out.String(); s != expect { + t.Errorf("expected output to be %q, got %q", expect, s) + } +} From 0d769ae93b394a219668a8f76ec66f3197117958 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 22 Jan 2024 12:03:48 -0300 Subject: [PATCH 06/13] test: more tests Signed-off-by: Carlos Alexandro Becker --- cmd_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/cmd_test.go b/cmd_test.go index 5cc35e07..7070a92d 100644 --- a/cmd_test.go +++ b/cmd_test.go @@ -19,6 +19,9 @@ func TestCommandNoPty(t *testing.T) { cmd := Command(s, "env") cmd.SetEnv([]string{"HELLO=hello"}) + if len(cmd.Environ()) != 1 { + Fatal(s, "unexpected cmd environ:", cmd.Environ()) + } if err := cmd.Run(); err != nil { Fatal(s, "env:", err) } @@ -44,3 +47,48 @@ func TestCommandNoPty(t *testing.T) { t.Errorf("expected output to be %q, got %q", expect, s) } } + +func TestCommandPty(t *testing.T) { + tmp := t.TempDir() + srv := &ssh.Server{ + Handler: func(s ssh.Session) { + if err := Command(s, "echo", "hello").Run(); err != nil { + Fatal(s, "echo:", err) + } + + cmd := Command(s, "env") + cmd.SetEnv([]string{"HELLO=hello"}) + if err := cmd.Run(); err != nil { + Fatal(s, "env:", err) + } + + cmd = Command(s, "pwd") + cmd.SetDir(tmp) + if err := cmd.Run(); err != nil { + Fatal(s, "pwd:", err) + } + }, + } + if err := ssh.AllocatePty()(srv); err != nil { + t.Fatalf("expected no error, got %v", err) + } + + sess := testsession.New(t, srv, nil) + if err := sess.RequestPty("xterm", 500, 200, nil); err != nil { + t.Fatalf("expected no error, got %v", err) + } + + var out bytes.Buffer + sess.Stdout = &out + if err := sess.Run(""); err != nil { + t.Errorf("expected no error, got %v", err) + } + expect := strings.Join([]string{ + "hello", + "HELLO=hello", + tmp, + }, "\r\n") + "\r\n" + if s := out.String(); s != expect { + t.Errorf("expected output to be %q, got %q", expect, s) + } +} From 7491d08d6e4cdbab128d17763886184af73ae900 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 22 Jan 2024 13:20:30 -0300 Subject: [PATCH 07/13] test: more tests Signed-off-by: Carlos Alexandro Becker --- cmd_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/cmd_test.go b/cmd_test.go index 7070a92d..eb63a1f1 100644 --- a/cmd_test.go +++ b/cmd_test.go @@ -14,7 +14,7 @@ func TestCommandNoPty(t *testing.T) { sess := testsession.New(t, &ssh.Server{ Handler: func(s ssh.Session) { if err := Command(s, "echo", "hello").Run(); err != nil { - Fatal(s, "echo:", err) + Fatal(s, err) } cmd := Command(s, "env") @@ -23,13 +23,17 @@ func TestCommandNoPty(t *testing.T) { Fatal(s, "unexpected cmd environ:", cmd.Environ()) } if err := cmd.Run(); err != nil { - Fatal(s, "env:", err) + Fatal(s, err) } cmd = Command(s, "pwd") cmd.SetDir(tmp) + // these should do nothing... + cmd.SetStderr(nil) + cmd.SetStdin(nil) + cmd.SetStdout(nil) if err := cmd.Run(); err != nil { - Fatal(s, "pwd:", err) + Fatal(s, err) } }, }, nil) @@ -53,19 +57,23 @@ func TestCommandPty(t *testing.T) { srv := &ssh.Server{ Handler: func(s ssh.Session) { if err := Command(s, "echo", "hello").Run(); err != nil { - Fatal(s, "echo:", err) + Fatal(s, err) } cmd := Command(s, "env") cmd.SetEnv([]string{"HELLO=hello"}) if err := cmd.Run(); err != nil { - Fatal(s, "env:", err) + Fatal(s, err) } cmd = Command(s, "pwd") cmd.SetDir(tmp) + // these should do nothing... + cmd.SetStderr(nil) + cmd.SetStdin(nil) + cmd.SetStdout(nil) if err := cmd.Run(); err != nil { - Fatal(s, "pwd:", err) + Fatal(s, err) } }, } @@ -92,3 +100,31 @@ func TestCommandPty(t *testing.T) { t.Errorf("expected output to be %q, got %q", expect, s) } } + +func TestCommandPtyError(t *testing.T) { + srv := &ssh.Server{ + Handler: func(s ssh.Session) { + if err := Command(s, "nopenopenope").Run(); err != nil { + Fatal(s, err) + } + }, + } + if err := ssh.AllocatePty()(srv); err != nil { + t.Fatalf("expected no error, got %v", err) + } + + sess := testsession.New(t, srv, nil) + if err := sess.RequestPty("xterm", 500, 200, nil); err != nil { + t.Fatalf("expected no error, got %v", err) + } + + var out bytes.Buffer + sess.Stderr = &out + if err := sess.Run(""); err == nil { + t.Errorf("expected an error, got nil") + } + expect := `exec: "nopenopenope"` + if s := out.String(); !strings.Contains(s, expect) { + t.Errorf("expected output to contain %q, got %q", expect, s) + } +} From ee092f051e2651405d103a8a5c994414e81c5b5f Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 22 Jan 2024 13:47:02 -0300 Subject: [PATCH 08/13] fix: improve tests Signed-off-by: Carlos Alexandro Becker --- cmd_test.go | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/cmd_test.go b/cmd_test.go index eb63a1f1..264f6dc3 100644 --- a/cmd_test.go +++ b/cmd_test.go @@ -37,17 +37,19 @@ func TestCommandNoPty(t *testing.T) { } }, }, nil) - var out bytes.Buffer - sess.Stdout = &out + var stdout bytes.Buffer + var stderr bytes.Buffer + sess.Stdout = &stdout + sess.Stderr = &stderr if err := sess.Run(""); err != nil { - t.Errorf("expected no error, got %v", err) + t.Errorf("expected no error, got %v: %s", err, stderr.String()) } expect := strings.Join([]string{ "hello", "HELLO=hello", tmp, }, "\n") + "\n" - if s := out.String(); s != expect { + if s := stdout.String(); s != expect { t.Errorf("expected output to be %q, got %q", expect, s) } } @@ -86,17 +88,19 @@ func TestCommandPty(t *testing.T) { t.Fatalf("expected no error, got %v", err) } - var out bytes.Buffer - sess.Stdout = &out + var stdout bytes.Buffer + var stderr bytes.Buffer + sess.Stdout = &stdout + sess.Stderr = &stderr if err := sess.Run(""); err != nil { - t.Errorf("expected no error, got %v", err) + t.Errorf("expected no error, got %v: %s", err, stderr.String()) } expect := strings.Join([]string{ "hello", "HELLO=hello", tmp, }, "\r\n") + "\r\n" - if s := out.String(); s != expect { + if s := stdout.String(); s != expect { t.Errorf("expected output to be %q, got %q", expect, s) } } @@ -118,13 +122,13 @@ func TestCommandPtyError(t *testing.T) { t.Fatalf("expected no error, got %v", err) } - var out bytes.Buffer - sess.Stderr = &out + var stderr bytes.Buffer + sess.Stderr = &stderr if err := sess.Run(""); err == nil { t.Errorf("expected an error, got nil") } expect := `exec: "nopenopenope"` - if s := out.String(); !strings.Contains(s, expect) { + if s := stderr.String(); !strings.Contains(s, expect) { t.Errorf("expected output to contain %q, got %q", expect, s) } } From 0b7f07cf36c2a087423c7c443c4416ee49423f8b Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 22 Jan 2024 14:47:02 -0300 Subject: [PATCH 09/13] test: windows Signed-off-by: Carlos Alexandro Becker --- cmd_test.go | 117 +++++++++++++++++++++++++++------------------------- 1 file changed, 60 insertions(+), 57 deletions(-) diff --git a/cmd_test.go b/cmd_test.go index 264f6dc3..af90d97a 100644 --- a/cmd_test.go +++ b/cmd_test.go @@ -2,6 +2,7 @@ package wish import ( "bytes" + "runtime" "strings" "testing" @@ -13,28 +14,9 @@ func TestCommandNoPty(t *testing.T) { tmp := t.TempDir() sess := testsession.New(t, &ssh.Server{ Handler: func(s ssh.Session) { - if err := Command(s, "echo", "hello").Run(); err != nil { - Fatal(s, err) - } - - cmd := Command(s, "env") - cmd.SetEnv([]string{"HELLO=hello"}) - if len(cmd.Environ()) != 1 { - Fatal(s, "unexpected cmd environ:", cmd.Environ()) - } - if err := cmd.Run(); err != nil { - Fatal(s, err) - } - - cmd = Command(s, "pwd") - cmd.SetDir(tmp) - // these should do nothing... - cmd.SetStderr(nil) - cmd.SetStdin(nil) - cmd.SetStdout(nil) - if err := cmd.Run(); err != nil { - Fatal(s, err) - } + runEcho(s, "hello") + runEnv(s, []string{"HELLO=world"}) + runPwd(s, tmp) }, }, nil) var stdout bytes.Buffer @@ -44,39 +26,19 @@ func TestCommandNoPty(t *testing.T) { if err := sess.Run(""); err != nil { t.Errorf("expected no error, got %v: %s", err, stderr.String()) } - expect := strings.Join([]string{ - "hello", - "HELLO=hello", - tmp, - }, "\n") + "\n" - if s := stdout.String(); s != expect { - t.Errorf("expected output to be %q, got %q", expect, s) - } + out := stdout.String() + expectContains(t, out, "hello") + expectContains(t, out, "HELLO=world") + expectContains(t, out, tmp) } func TestCommandPty(t *testing.T) { tmp := t.TempDir() srv := &ssh.Server{ Handler: func(s ssh.Session) { - if err := Command(s, "echo", "hello").Run(); err != nil { - Fatal(s, err) - } - - cmd := Command(s, "env") - cmd.SetEnv([]string{"HELLO=hello"}) - if err := cmd.Run(); err != nil { - Fatal(s, err) - } - - cmd = Command(s, "pwd") - cmd.SetDir(tmp) - // these should do nothing... - cmd.SetStderr(nil) - cmd.SetStdin(nil) - cmd.SetStdout(nil) - if err := cmd.Run(); err != nil { - Fatal(s, err) - } + runEcho(s, "hello") + runEnv(s, []string{"HELLO=world"}) + runPwd(s, tmp) }, } if err := ssh.AllocatePty()(srv); err != nil { @@ -95,14 +57,10 @@ func TestCommandPty(t *testing.T) { if err := sess.Run(""); err != nil { t.Errorf("expected no error, got %v: %s", err, stderr.String()) } - expect := strings.Join([]string{ - "hello", - "HELLO=hello", - tmp, - }, "\r\n") + "\r\n" - if s := stdout.String(); s != expect { - t.Errorf("expected output to be %q, got %q", expect, s) - } + out := stdout.String() + expectContains(t, out, "hello") + expectContains(t, out, "HELLO=world") + expectContains(t, out, tmp) } func TestCommandPtyError(t *testing.T) { @@ -132,3 +90,48 @@ func TestCommandPtyError(t *testing.T) { t.Errorf("expected output to contain %q, got %q", expect, s) } } + +func runEcho(s ssh.Session, str string) { + cmd := Command(s, "echo", str) + if runtime.GOOS == "windows" { + cmd = Command(s, "cmd", "/C", "echo", str) + } + // these should do nothing... + cmd.SetStderr(nil) + cmd.SetStdin(nil) + cmd.SetStdout(nil) + if err := cmd.Run(); err != nil { + Fatal(s, err) + } +} + +func runEnv(s ssh.Session, env []string) { + cmd := Command(s, "env") + if runtime.GOOS == "windows" { + cmd = Command(s, "cmd", "/C", "set") + } + cmd.SetEnv(env) + if err := cmd.Run(); err != nil { + Fatal(s, err) + } + if len(cmd.Environ()) == 0 { + Fatal(s, "cmd.Environ() should not be empty") + } +} + +func runPwd(s ssh.Session, dir string) { + cmd := Command(s, "pwd") + if runtime.GOOS == "windows" { + cmd = Command(s, "cmd", "/C", "cd") + } + cmd.SetDir(dir) + if err := cmd.Run(); err != nil { + Fatal(s, err) + } +} + +func expectContains(tb testing.TB, s, substr string) { + if !strings.Contains(s, substr) { + tb.Errorf("expected output %q to contain %q", s, substr) + } +} From 32aeafcecb9d2832f7a48258ed4c3ee18cecec9a Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 22 Jan 2024 14:55:22 -0300 Subject: [PATCH 10/13] fix: review Signed-off-by: Carlos Alexandro Becker --- cmd.go | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/cmd.go b/cmd.go index 6f1aec85..2c76b61e 100644 --- a/cmd.go +++ b/cmd.go @@ -1,21 +1,23 @@ package wish import ( + "context" "fmt" "io" "os/exec" "runtime" "time" + tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/ssh" ) -// Command sets stdin, stdout, and stderr to the current session's PTY slave. +// CommandContext is like Command but includes a context. // // 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...) +func CommandContext(ctx context.Context, s ssh.Session, name string, args ...string) *Cmd { + c := exec.CommandContext(ctx, name, args...) pty, _, ok := s.Pty() if !ok { c.Stdin, c.Stdout, c.Stderr = s, s, s @@ -25,6 +27,16 @@ func Command(s ssh.Session, name string, args ...string) *Cmd { return &Cmd{c, &pty} } +// 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. +// +// This will call CommandContext using the session's Context. +func Command(s ssh.Session, name string, args ...string) *Cmd { + return CommandContext(s.Context(), s, name, args...) +} + // Cmd wraps a *exec.Cmd and a ssh.Pty so a command can be properly run. type Cmd struct { cmd *exec.Cmd @@ -46,15 +58,6 @@ func (c *Cmd) SetDir(dir string) { c.cmd.Dir = dir } -// SetStderr conforms with tea.ExecCommand. -func (*Cmd) SetStderr(io.Writer) {} - -// SetStdin conforms with tea.ExecCommand. -func (*Cmd) SetStdin(io.Reader) {} - -// SetStdout conforms with tea.ExecCommand. -func (*Cmd) SetStdout(io.Writer) {} - // Run runs the program and waits for it to finish. func (c *Cmd) Run() error { if c.pty == nil { @@ -83,3 +86,14 @@ func (c *Cmd) Run() error { } return nil } + +var _ tea.ExecCommand = &Cmd{} + +// SetStderr conforms with tea.ExecCommand. +func (*Cmd) SetStderr(io.Writer) {} + +// SetStdin conforms with tea.ExecCommand. +func (*Cmd) SetStdin(io.Reader) {} + +// SetStdout conforms with tea.ExecCommand. +func (*Cmd) SetStdout(io.Writer) {} From 1681ea4e2581c9bf285377ac4d2fa6141f59f984 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 22 Jan 2024 15:51:33 -0300 Subject: [PATCH 11/13] fix: improve cmd --- cmd.go | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/cmd.go b/cmd.go index 2c76b61e..e55b6a5e 100644 --- a/cmd.go +++ b/cmd.go @@ -17,14 +17,15 @@ import ( // If the current session does not have a PTY, it sets them to the session // itself. func CommandContext(ctx context.Context, s ssh.Session, name string, args ...string) *Cmd { - c := exec.CommandContext(ctx, name, args...) + cmd := exec.CommandContext(ctx, name, args...) pty, _, ok := s.Pty() if !ok { - c.Stdin, c.Stdout, c.Stderr = s, s, s - return &Cmd{cmd: c} + cmd.Stdin, cmd.Stdout, cmd.Stderr = s, s, s + return &Cmd{cmd: cmd} } - return &Cmd{c, &pty} + cmd.Env = append(cmd.Environ(), "SSH_TTY="+pty.Name(), fmt.Sprintf("TERM=%s", pty.Term)) + return &Cmd{cmd, &pty} } // Command sets stdin, stdout, and stderr to the current session's PTY slave. @@ -67,24 +68,22 @@ func (c *Cmd) Run() error { if err := c.pty.Start(c.cmd); err != nil { return err } - start := time.Now() + if runtime.GOOS == "windows" { + start := time.Now() for c.cmd.ProcessState == nil { if time.Since(start) > time.Second*10 { return fmt.Errorf("could not start process") } time.Sleep(100 * time.Millisecond) } - 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 - } + return nil } - return nil + + return c.cmd.Wait() } var _ tea.ExecCommand = &Cmd{} From 98030fa6cfaaabdb27d90aae8d43da8300df5957 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Fri, 26 Jan 2024 15:06:53 -0300 Subject: [PATCH 12/13] fix: create a new pty for exec.Cmd (#230) * fix: create a new pty for exec.Cmd Signed-off-by: Carlos Alexandro Becker * fix: improvements * fix: test * fix: windows Signed-off-by: Carlos Alexandro Becker * fix: unneeded err Signed-off-by: Carlos Alexandro Becker * fix Signed-off-by: Carlos Alexandro Becker * fix: resize && race Signed-off-by: Carlos Alexandro Becker * fix: windows/linux Signed-off-by: Carlos Alexandro Becker * test: ignore on windows for now Signed-off-by: Carlos Alexandro Becker * fix: sync Signed-off-by: Carlos Alexandro Becker * test: hammer Signed-off-by: Carlos Alexandro Becker * chore: import --------- Signed-off-by: Carlos Alexandro Becker --- cmd.go | 41 ++++----------------- cmd_darwin.go | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++ cmd_test.go | 10 ++++++ cmd_unix.go | 13 +++++++ cmd_windows.go | 29 +++++++++++++++ go.mod | 6 ++-- 6 files changed, 158 insertions(+), 37 deletions(-) create mode 100644 cmd_darwin.go create mode 100644 cmd_unix.go create mode 100644 cmd_windows.go diff --git a/cmd.go b/cmd.go index e55b6a5e..18868dd0 100644 --- a/cmd.go +++ b/cmd.go @@ -2,11 +2,8 @@ package wish import ( "context" - "fmt" "io" "os/exec" - "runtime" - "time" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/ssh" @@ -18,14 +15,7 @@ import ( // itself. func CommandContext(ctx context.Context, s ssh.Session, name string, args ...string) *Cmd { cmd := exec.CommandContext(ctx, name, args...) - pty, _, ok := s.Pty() - if !ok { - cmd.Stdin, cmd.Stdout, cmd.Stderr = s, s, s - return &Cmd{cmd: cmd} - } - - cmd.Env = append(cmd.Environ(), "SSH_TTY="+pty.Name(), fmt.Sprintf("TERM=%s", pty.Term)) - return &Cmd{cmd, &pty} + return &Cmd{s, cmd} } // Command sets stdin, stdout, and stderr to the current session's PTY slave. @@ -40,8 +30,8 @@ func Command(s ssh.Session, name string, args ...string) *Cmd { // 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 + sess ssh.Session + cmd *exec.Cmd } // SetDir set the underlying exec.Cmd env. @@ -61,29 +51,12 @@ func (c *Cmd) SetDir(dir string) { // Run runs the program and waits for it to finish. func (c *Cmd) Run() error { - if c.pty == nil { + ppty, winCh, ok := c.sess.Pty() + if !ok { + c.cmd.Stdin, c.cmd.Stdout, c.cmd.Stderr = c.sess, c.sess, c.sess return c.cmd.Run() } - - if err := c.pty.Start(c.cmd); err != nil { - return err - } - - if runtime.GOOS == "windows" { - start := time.Now() - for c.cmd.ProcessState == nil { - if time.Since(start) > time.Second*10 { - return fmt.Errorf("could not start process") - } - time.Sleep(100 * time.Millisecond) - } - if !c.cmd.ProcessState.Success() { - return fmt.Errorf("process failed: exit %d", c.cmd.ProcessState.ExitCode()) - } - return nil - } - - return c.cmd.Wait() + return c.doRun(ppty, winCh) } var _ tea.ExecCommand = &Cmd{} diff --git a/cmd_darwin.go b/cmd_darwin.go new file mode 100644 index 00000000..7d690c9f --- /dev/null +++ b/cmd_darwin.go @@ -0,0 +1,96 @@ +//go:build darwin +// +build darwin + +package wish + +import ( + "errors" + "io" + + "github.com/charmbracelet/log" + "github.com/charmbracelet/ssh" + "github.com/creack/pty" + "github.com/muesli/cancelreader" + "golang.org/x/term" +) + +// on macOS, the slave pty is killed once exec finishes. +// since we're using it for the ssh session, this would render +// the pty and the session unusable. +// so, we need to create another pty, and run the Cmd on it instead. +func (c *Cmd) doRun(ppty ssh.Pty, winCh <-chan ssh.Window) error { + done := make(chan struct{}, 1) + go func() { + <-done + close(done) + }() + ptmxClose := make(chan struct{}, 1) + ptmx, err := pty.Start(c.cmd) + if err != nil { + return err + } + defer func() { + if err := ptmx.Close(); err != nil { + log.Warn("could not close pty", "err", err) + } + ptmxClose <- struct{}{} + close(ptmxClose) + }() + + // setup resizes + go func() { + for { + select { + case <-ptmxClose: + return + case w := <-winCh: + log.Infof("resize %d %d", w.Height, w.Width) + if err := pty.Setsize(ptmx, &pty.Winsize{ + Rows: uint16(w.Height), + Cols: uint16(w.Width), + }); err != nil { + log.Warn("could not set term size", "err", err) + } + } + } + }() + if err := pty.InheritSize(ppty.Slave, ptmx); err != nil { + return err + } + + // put the ssh session's pty in raw mode + oldState, err := term.MakeRaw(int(ppty.Slave.Fd())) + if err != nil { + return err + } + defer func() { + if err := term.Restore(int(ppty.Slave.Fd()), oldState); err != nil { + log.Error("could not restore terminal", "err", err) + } + }() + + // we'll need to be able to cancel the reader, otherwise the copy + // from ptmx will eat the next keypress after the exec exits. + cancelSlave, err := cancelreader.NewReader(ppty.Slave) + if err != nil { + return err + } + defer func() { cancelSlave.Cancel() }() + + // sync io + go func() { + defer func() { done <- struct{}{} }() + if _, err := io.Copy(ptmx, cancelSlave); err != nil { + if errors.Is(err, io.EOF) || errors.Is(err, cancelreader.ErrCanceled) { + // safe to ignore + return + } + log.Warn("failed to copy", "err", err) + } + }() + if _, err := io.Copy(ppty.Slave, ptmx); err != nil && !errors.Is(err, io.EOF) { + return err + } + + return c.cmd.Wait() +} diff --git a/cmd_test.go b/cmd_test.go index af90d97a..32d20a1c 100644 --- a/cmd_test.go +++ b/cmd_test.go @@ -5,6 +5,7 @@ import ( "runtime" "strings" "testing" + "time" "github.com/charmbracelet/ssh" "github.com/charmbracelet/wish/testsession" @@ -33,12 +34,18 @@ func TestCommandNoPty(t *testing.T) { } func TestCommandPty(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip() + } tmp := t.TempDir() srv := &ssh.Server{ Handler: func(s ssh.Session) { runEcho(s, "hello") runEnv(s, []string{"HELLO=world"}) runPwd(s, tmp) + // for some reason sometimes on macos github action runners, + // it cuts parts of the output. + time.Sleep(100 * time.Millisecond) }, } if err := ssh.AllocatePty()(srv); err != nil { @@ -64,6 +71,9 @@ func TestCommandPty(t *testing.T) { } func TestCommandPtyError(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip() + } srv := &ssh.Server{ Handler: func(s ssh.Session) { if err := Command(s, "nopenopenope").Run(); err != nil { diff --git a/cmd_unix.go b/cmd_unix.go new file mode 100644 index 00000000..b5c6bf9e --- /dev/null +++ b/cmd_unix.go @@ -0,0 +1,13 @@ +//go:build dragonfly || freebsd || linux || netbsd || openbsd || solaris +// +build dragonfly freebsd linux netbsd openbsd solaris + +package wish + +import "github.com/charmbracelet/ssh" + +func (c *Cmd) doRun(ppty ssh.Pty, _ <-chan ssh.Window) error { + if err := ppty.Start(c.cmd); err != nil { + return err + } + return c.cmd.Wait() +} diff --git a/cmd_windows.go b/cmd_windows.go new file mode 100644 index 00000000..3f25eab0 --- /dev/null +++ b/cmd_windows.go @@ -0,0 +1,29 @@ +//go:build windows +// +build windows + +package wish + +import ( + "fmt" + "time" + + "github.com/charmbracelet/ssh" +) + +func (c *Cmd) doRun(ppty ssh.Pty, _ <-chan ssh.Window) error { + if err := ppty.Start(c.cmd); err != nil { + return err + } + + start := time.Now() + for c.cmd.ProcessState == nil { + if time.Since(start) > time.Second*10 { + return fmt.Errorf("could not start process") + } + time.Sleep(100 * time.Millisecond) + } + if !c.cmd.ProcessState.Success() { + return fmt.Errorf("process failed: exit %d", c.cmd.ProcessState.ExitCode()) + } + return nil +} diff --git a/go.mod b/go.mod index dac76339..2de2dc6e 100644 --- a/go.mod +++ b/go.mod @@ -8,13 +8,16 @@ require ( github.com/charmbracelet/lipgloss v0.9.1 github.com/charmbracelet/log v0.3.1 github.com/charmbracelet/ssh v0.0.0-20240118173142-6d7cf11c8371 + github.com/creack/pty v1.1.21 github.com/go-git/go-git/v5 v5.11.0 github.com/google/go-cmp v0.6.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/matryer/is v1.4.1 + github.com/muesli/cancelreader v0.2.2 github.com/muesli/termenv v0.15.2 golang.org/x/crypto v0.18.0 golang.org/x/sync v0.6.0 + golang.org/x/term v0.16.0 golang.org/x/time v0.5.0 ) @@ -28,7 +31,6 @@ require ( github.com/charmbracelet/x/exp/term v0.0.0-20240117031359-6e25c76a1efe // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect - github.com/creack/pty v1.1.21 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect @@ -42,7 +44,6 @@ require ( github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect - github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect @@ -54,7 +55,6 @@ require ( golang.org/x/mod v0.13.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/sys v0.16.0 // indirect - golang.org/x/term v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.14.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect From 10790a88207796cd39a20650e55e0a8bb2aa295d Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 29 Jan 2024 15:49:30 -0300 Subject: [PATCH 13/13] fix: update ssh Signed-off-by: Carlos Alexandro Becker --- cmd_darwin.go | 96 --------------------------------------------------- cmd_unix.go | 4 +-- go.mod | 8 ++--- go.sum | 4 +-- 4 files changed, 8 insertions(+), 104 deletions(-) delete mode 100644 cmd_darwin.go diff --git a/cmd_darwin.go b/cmd_darwin.go deleted file mode 100644 index 7d690c9f..00000000 --- a/cmd_darwin.go +++ /dev/null @@ -1,96 +0,0 @@ -//go:build darwin -// +build darwin - -package wish - -import ( - "errors" - "io" - - "github.com/charmbracelet/log" - "github.com/charmbracelet/ssh" - "github.com/creack/pty" - "github.com/muesli/cancelreader" - "golang.org/x/term" -) - -// on macOS, the slave pty is killed once exec finishes. -// since we're using it for the ssh session, this would render -// the pty and the session unusable. -// so, we need to create another pty, and run the Cmd on it instead. -func (c *Cmd) doRun(ppty ssh.Pty, winCh <-chan ssh.Window) error { - done := make(chan struct{}, 1) - go func() { - <-done - close(done) - }() - ptmxClose := make(chan struct{}, 1) - ptmx, err := pty.Start(c.cmd) - if err != nil { - return err - } - defer func() { - if err := ptmx.Close(); err != nil { - log.Warn("could not close pty", "err", err) - } - ptmxClose <- struct{}{} - close(ptmxClose) - }() - - // setup resizes - go func() { - for { - select { - case <-ptmxClose: - return - case w := <-winCh: - log.Infof("resize %d %d", w.Height, w.Width) - if err := pty.Setsize(ptmx, &pty.Winsize{ - Rows: uint16(w.Height), - Cols: uint16(w.Width), - }); err != nil { - log.Warn("could not set term size", "err", err) - } - } - } - }() - if err := pty.InheritSize(ppty.Slave, ptmx); err != nil { - return err - } - - // put the ssh session's pty in raw mode - oldState, err := term.MakeRaw(int(ppty.Slave.Fd())) - if err != nil { - return err - } - defer func() { - if err := term.Restore(int(ppty.Slave.Fd()), oldState); err != nil { - log.Error("could not restore terminal", "err", err) - } - }() - - // we'll need to be able to cancel the reader, otherwise the copy - // from ptmx will eat the next keypress after the exec exits. - cancelSlave, err := cancelreader.NewReader(ppty.Slave) - if err != nil { - return err - } - defer func() { cancelSlave.Cancel() }() - - // sync io - go func() { - defer func() { done <- struct{}{} }() - if _, err := io.Copy(ptmx, cancelSlave); err != nil { - if errors.Is(err, io.EOF) || errors.Is(err, cancelreader.ErrCanceled) { - // safe to ignore - return - } - log.Warn("failed to copy", "err", err) - } - }() - if _, err := io.Copy(ppty.Slave, ptmx); err != nil && !errors.Is(err, io.EOF) { - return err - } - - return c.cmd.Wait() -} diff --git a/cmd_unix.go b/cmd_unix.go index b5c6bf9e..7cec06c5 100644 --- a/cmd_unix.go +++ b/cmd_unix.go @@ -1,5 +1,5 @@ -//go:build dragonfly || freebsd || linux || netbsd || openbsd || solaris -// +build dragonfly freebsd linux netbsd openbsd solaris +//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris +// +build darwin dragonfly freebsd linux netbsd openbsd solaris package wish diff --git a/go.mod b/go.mod index 2de2dc6e..79187451 100644 --- a/go.mod +++ b/go.mod @@ -7,17 +7,14 @@ require ( github.com/charmbracelet/keygen v0.5.0 github.com/charmbracelet/lipgloss v0.9.1 github.com/charmbracelet/log v0.3.1 - github.com/charmbracelet/ssh v0.0.0-20240118173142-6d7cf11c8371 - github.com/creack/pty v1.1.21 + github.com/charmbracelet/ssh v0.0.0-20240129182809-006ab784ccc7 github.com/go-git/go-git/v5 v5.11.0 github.com/google/go-cmp v0.6.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/matryer/is v1.4.1 - github.com/muesli/cancelreader v0.2.2 github.com/muesli/termenv v0.15.2 golang.org/x/crypto v0.18.0 golang.org/x/sync v0.6.0 - golang.org/x/term v0.16.0 golang.org/x/time v0.5.0 ) @@ -31,6 +28,7 @@ require ( github.com/charmbracelet/x/exp/term v0.0.0-20240117031359-6e25c76a1efe // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/creack/pty v1.1.21 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect @@ -44,6 +42,7 @@ require ( github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect @@ -55,6 +54,7 @@ require ( golang.org/x/mod v0.13.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/sys v0.16.0 // indirect + golang.org/x/term v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.14.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect diff --git a/go.sum b/go.sum index 6f79b4ef..356d5c3a 100644 --- a/go.sum +++ b/go.sum @@ -19,8 +19,8 @@ github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1 github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= github.com/charmbracelet/log v0.3.1 h1:TjuY4OBNbxmHWSwO3tosgqs5I3biyY8sQPny/eCMTYw= github.com/charmbracelet/log v0.3.1/go.mod h1:OR4E1hutLsax3ZKpXbgUqPtTjQfrh1pG3zwHGWuuq8g= -github.com/charmbracelet/ssh v0.0.0-20240118173142-6d7cf11c8371 h1:lgr2JbKDeq13Ar9gwmwELlJAF6R13pP3Kp37C9KLVM8= -github.com/charmbracelet/ssh v0.0.0-20240118173142-6d7cf11c8371/go.mod h1:l/6/exIt0QMHEW5IiqoY9HyPlSuXAm6xkngFIJgiEYI= +github.com/charmbracelet/ssh v0.0.0-20240129182809-006ab784ccc7 h1:DL71svOeh8OSFdPc8GOAX9RMKb9+Ss0ldENOOkrL8/s= +github.com/charmbracelet/ssh v0.0.0-20240129182809-006ab784ccc7/go.mod h1:l/6/exIt0QMHEW5IiqoY9HyPlSuXAm6xkngFIJgiEYI= 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/term v0.0.0-20240117031359-6e25c76a1efe h1:HeRgHWxOTu7l73rKsa5BRAeaUenmNyomiPCUHXv/y14=