Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use real pty #197

Merged
merged 38 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
0b36a69
feat: use pty
caarlos0 Dec 4, 2023
aa0bb66
fix: deps
caarlos0 Dec 4, 2023
fc89255
fix: deps
caarlos0 Dec 4, 2023
0868b14
fix: update example
caarlos0 Dec 4, 2023
46447ee
fix: deps
caarlos0 Dec 4, 2023
9ba7e36
docs: fix example
caarlos0 Dec 5, 2023
ab27fe2
feat: pass lipgloss.Renderer down to the tea.App
caarlos0 Dec 8, 2023
020651e
Merge remote-tracking branch 'origin/main' into using-pty
caarlos0 Dec 8, 2023
c6e19fc
Merge remote-tracking branch 'origin/main' into using-pty
caarlos0 Dec 12, 2023
a70d0bd
fix: go mod tidy
caarlos0 Dec 12, 2023
fd4337e
improvements
caarlos0 Jan 4, 2024
488fd2f
Merge remote-tracking branch 'origin/main' into using-pty
caarlos0 Jan 4, 2024
c9ad8ee
Merge remote-tracking branch 'origin/main' into using-pty
caarlos0 Jan 5, 2024
83cc777
fix: pty
caarlos0 Jan 5, 2024
60e8254
fix: better diff
caarlos0 Jan 5, 2024
2cf714d
chore: typo
caarlos0 Jan 5, 2024
59c5884
fix: godocs
caarlos0 Jan 5, 2024
9ab015a
fix: allocate pty on macos
caarlos0 Jan 8, 2024
50a706e
fix: improvements
caarlos0 Jan 8, 2024
411f301
chore: godoc
caarlos0 Jan 8, 2024
9d68adf
Merge remote-tracking branch 'origin/main' into using-pty
caarlos0 Jan 9, 2024
47074ac
fix: review
caarlos0 Jan 9, 2024
5e7f41a
fix: example
caarlos0 Jan 9, 2024
4c7fb4f
fix: tea program handler
caarlos0 Jan 9, 2024
0431178
fix: examples
caarlos0 Jan 9, 2024
aed4f59
Merge remote-tracking branch 'origin/main' into using-pty
caarlos0 Jan 9, 2024
c40b251
Merge remote-tracking branch 'origin/main' into using-pty
caarlos0 Jan 9, 2024
d7ee1ec
refactor: improve p!=nil handling
caarlos0 Jan 9, 2024
c0d4269
fix: ensure session envs are available to renderer (#223)
aymanbagabas Jan 9, 2024
73189b2
fix: rename func to makeoptions
caarlos0 Jan 10, 2024
569d089
fix: not too much breaking
caarlos0 Jan 10, 2024
b0c56ba
chore: fix gitignore
caarlos0 Jan 10, 2024
c9668e0
Merge remote-tracking branch 'origin/main' into using-pty
caarlos0 Jan 17, 2024
91c322b
fix: dep
caarlos0 Jan 17, 2024
cd60d40
fix: update charmbracelet/ssh
aymanbagabas Jan 18, 2024
12a7a6a
fix: update dep
caarlos0 Jan 18, 2024
731c66e
chore: go mod tidy
caarlos0 Jan 18, 2024
20ca97a
chore: update example
caarlos0 Jan 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ coverage.txt

# MacOS specific
.DS_Store
id_ed25519
id_ed25519.pub
118 changes: 64 additions & 54 deletions bubbletea/tea.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package bubbletea

import (
"context"
"io"

"github.com/aymanbagabas/go-pty"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/log"
Expand All @@ -22,7 +24,7 @@ type BubbleTeaHandler = Handler // nolint: revive
// Handler is the function Bubble Tea apps implement to hook into the
// SSH Middleware. This will create a new tea.Program for every connection and
// start it with the tea.ProgramOptions returned.
type Handler func(ssh.Session) (tea.Model, []tea.ProgramOption)
type Handler func(ssh.Session, *lipgloss.Renderer) (tea.Model, []tea.ProgramOption)

// ProgramHandler is the function Bubble Tea apps implement to hook into the SSH
// Middleware. This should return a new tea.Program. This handler is different
Expand All @@ -33,73 +35,81 @@ type Handler func(ssh.Session) (tea.Model, []tea.ProgramOption)
// otherwise the program will not function properly.
type ProgramHandler func(ssh.Session) *tea.Program

// Middleware takes a Handler and hooks the input and output for the
// ssh.Session into the tea.Program. It also captures window resize events and
// sends them to the tea.Program as tea.WindowSizeMsgs. By default a 256 color
// profile will be used when rendering with Lip Gloss.
func Middleware(bth Handler) wish.Middleware {
return MiddlewareWithColorProfile(bth, termenv.ANSI256)
}

// MiddlewareWithColorProfile allows you to specify the number of colors
// returned by the server when using Lip Gloss. The number of colors supported
// by an SSH client's terminal cannot be detected by the server but this will
// allow for manually setting the color profile on all SSH connections.
func MiddlewareWithColorProfile(bth Handler, cp termenv.Profile) wish.Middleware {
h := func(s ssh.Session) *tea.Program {
m, opts := bth(s)
if m == nil {
return nil
}
opts = append(opts, tea.WithInput(s), tea.WithOutput(s))
return tea.NewProgram(m, opts...)
}
return MiddlewareWithProgramHandler(h, cp)
}

// MiddlewareWithProgramHandler allows you to specify the ProgramHandler to be
// able to access the underlying tea.Program. This is useful for creating custom
// middlewars that need access to tea.Program for instance to use p.Send() to
// send messages to tea.Program.
//
// Make sure to set the tea.WithInput and tea.WithOutput to the ssh.Session
// otherwise the program will not function properly.
func MiddlewareWithProgramHandler(bth ProgramHandler, cp termenv.Profile) wish.Middleware {
// XXX: This is a hack to make sure the default Termenv output color
// profile is set before the program starts. Ideally, we want a Lip Gloss
// renderer per session.
lipgloss.SetColorProfile(cp)
func Middleware(bth Handler) wish.Middleware {
return func(sh ssh.Handler) ssh.Handler {
return func(s ssh.Session) {
p := bth(s)
if p != nil {
_, windowChanges, _ := s.Pty()
ctx, cancel := context.WithCancel(s.Context())
go func() {
for {
select {
case <-ctx.Done():
if p != nil {
p.Quit()
return
}
case w := <-windowChanges:
if p != nil {
p.Send(tea.WindowSizeMsg{Width: w.Width, Height: w.Height})
}
opts := []tea.ProgramOption{tea.WithInput(s), tea.WithOutput(s)}

tty, windowChanges, ok := s.Pty()
if !ok {
wish.Fatalln(s, "no active terminal, skipping")
return
}

upty, ok := tty.Pty.(pty.UnixPty)
if ok {
f := upty.Slave()
out := termenv.NewOutput(f, termenv.WithColorCache(true))
opts = []tea.ProgramOption{tea.WithInput(f), tea.WithOutput(out)}
caarlos0 marked this conversation as resolved.
Show resolved Hide resolved
}

renderer := lipgloss.NewRenderer(tty, termenv.WithColorCache(true))

m, hopts := bth(s, renderer)
if m == nil {
log.Error("no model returned by the handler")
return
}

opts = append(opts, hopts...)

p := tea.NewProgram(m, opts...)
ctx, cancel := context.WithCancel(s.Context())
go func() {
for {
select {
case <-ctx.Done():
if p != nil {
p.Quit()
return
}
case w := <-windowChanges:
if p != nil {
p.Send(tea.WindowSizeMsg{Width: w.Width, Height: w.Height})
}
}
}()
if _, err := p.Run(); err != nil {
log.Error("app exit with error", "error", err)
}
// p.Kill() will force kill the program if it's still running,
// and restore the terminal to its original state in case of a
// tui crash
p.Kill()
cancel()
}()
if _, err := p.Run(); err != nil {
log.Error("app exit with error", "error", err)
}
// p.Kill() will force kill the program if it's still running,
// and restore the terminal to its original state in case of a
// tui crash
p.Kill()
cancel()
if err := tty.Close(); err != nil {
log.Error("could not close pty", "error", err)
return
}

sh(s)
}
}
}

// Command makes a *pty.Cmd executable with tea.Exec.
func Command(c *pty.Cmd) tea.ExecCommand { return &ptyCommand{c} }

type ptyCommand struct{ *pty.Cmd }

func (*ptyCommand) SetStderr(io.Writer) {} // noop
func (*ptyCommand) SetStdin(io.Reader) {} // noop
func (*ptyCommand) SetStdout(io.Writer) {} // noop
2 changes: 2 additions & 0 deletions examples/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
id_ed25519*
file.txt
11 changes: 7 additions & 4 deletions examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ go 1.18

require (
github.com/charmbracelet/bubbles v0.16.1
github.com/charmbracelet/bubbletea v0.24.2
github.com/charmbracelet/bubbletea v0.24.3-0.20231207115923-e671b840f205
github.com/charmbracelet/lipgloss v0.9.1
github.com/charmbracelet/log v0.3.1
github.com/charmbracelet/ssh v0.0.0-20221117183211-483d43d97103
github.com/charmbracelet/ssh v0.0.0-20231203183338-c29875a2932c
github.com/charmbracelet/wish v0.5.0
github.com/muesli/termenv v0.15.2
github.com/spf13/cobra v1.7.0
Expand All @@ -20,14 +20,16 @@ require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymanbagabas/go-pty v0.2.1 // indirect
github.com/charmbracelet/keygen v0.5.0 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/creack/pty v1.1.15 // 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
github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/go-git/go-git/v5 v5.10.1 // indirect
github.com/go-git/go-git/v5 v5.11.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
Expand All @@ -45,11 +47,12 @@ require (
github.com/sergi/go-diff v1.1.0 // indirect
github.com/skeema/knownhosts v1.2.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/u-root/u-root v0.11.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.16.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/mod v0.13.0 // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/term v0.15.0 // indirect
Expand Down
Loading
Loading